// ─── Pipeline Stages ───────────────────────────────────────────────────────── // // 7 canonical stages (one optional). Document-signing stages (EOI, Reservation, // Contract) collapse "Sent + Signed" into one stage; the sub-status lives on // per-stage doc-status columns (`eoi_doc_status`, etc.) and is rendered as a // badge inside the kanban card. // // `nurturing` is built but disabled-by-default for ports that don't have // supply constraints (e.g. Port Nimara pre-launch). Admins enable it per port. export const PIPELINE_STAGES = [ 'enquiry', 'qualified', 'nurturing', 'eoi', 'reservation', 'deposit_paid', 'contract', ] as const; export type PipelineStage = (typeof PIPELINE_STAGES)[number]; /** * Sub-status values for document-signing stages (EOI, Reservation, Contract). * Stored on per-stage columns `eoi_doc_status` / `reservation_doc_status` / * `contract_doc_status` on the interests table. */ export const DOC_STATUSES = ['pending', 'sent', 'signed', 'declined', 'voided'] as const; export type DocStatus = (typeof DOC_STATUSES)[number]; export const STAGE_LABELS: Record = { enquiry: 'New Enquiry', qualified: 'Qualified', nurturing: 'Nurturing', eoi: 'EOI', reservation: 'Reservation', deposit_paid: 'Deposit Paid', contract: 'Contract', }; /** * Map legacy 9-stage enum values to their 7-stage equivalents. Audit logs * and any pre-migration data still carry the legacy values; this lets the * activity feed, audit diffs, and reporting render the modern label * without having to back-fill the underlying rows. * * Mirrors the migration applied in `seed-synthetic-data.ts` (and * documented in the 9→7 pipeline refactor): * details_sent → enquiry * in_communication → qualified * eoi_sent, eoi_signed → eoi (doc-status carries sent/signed sub-state) * deposit_10pct → deposit_paid * contract_sent, contract_signed → contract * completed → contract (with outcome=won) * open → enquiry (legacy alias for the initial stage) */ export const LEGACY_STAGE_REMAP: Record = { open: 'enquiry', details_sent: 'enquiry', in_communication: 'qualified', eoi_sent: 'eoi', eoi_signed: 'eoi', deposit_10pct: 'deposit_paid', contract_sent: 'contract', contract_signed: 'contract', completed: 'contract', }; /** * Resolve any stage-like string to a canonical 7-stage value. Returns * the modern stage as-is, maps legacy values via LEGACY_STAGE_REMAP, * and falls back to 'enquiry' for genuinely unknown values. */ export function canonicalizeStage(value: string | null | undefined): PipelineStage { if (!value) return 'enquiry'; if (PIPELINE_STAGES.includes(value as PipelineStage)) return value as PipelineStage; return LEGACY_STAGE_REMAP[value] ?? 'enquiry'; } /** * Human-friendly label for any stage-like string - modern or legacy. Use * this in any read surface (activity feed, audit diff, notification copy, * reports) that might be handed pre-migration data. */ export function stageLabelFor(value: string | null | undefined): string { return STAGE_LABELS[canonicalizeStage(value)]; } // Compact labels for cramped contexts (mobile chart axes, dense tables). export const STAGE_SHORT_LABELS: Record = { enquiry: 'Enquiry', qualified: 'Qual.', nurturing: 'Nurt.', eoi: 'EOI', reservation: 'Resv.', deposit_paid: 'Dep.', contract: 'Contract', }; export const STAGE_BADGE: Record = { enquiry: 'bg-slate-100 text-slate-700', qualified: 'bg-blue-100 text-blue-700', nurturing: 'bg-purple-100 text-purple-700', eoi: 'bg-indigo-100 text-indigo-700', reservation: 'bg-amber-100 text-amber-700', deposit_paid: 'bg-orange-100 text-orange-700', contract: 'bg-green-100 text-green-700', }; export const STAGE_DOT: Record = { enquiry: 'bg-slate-400', qualified: 'bg-blue-500', nurturing: 'bg-purple-500', eoi: 'bg-indigo-500', reservation: 'bg-amber-500', deposit_paid: 'bg-orange-500', contract: 'bg-green-500', }; // Default revenue-forecast probability weights per stage (0–1). // Editable per port via settings (`pipeline_weights`); these are the fallbacks. export const STAGE_WEIGHTS: Record = { enquiry: 0.05, qualified: 0.15, nurturing: 0.15, eoi: 0.4, reservation: 0.7, deposit_paid: 0.85, contract: 0.95, }; /** * Allowed transitions out of each stage. Skip-aheads (e.g. enquiry → * deposit_paid) are gated by the explicit `override:true` path in * `changeInterestStage` and surface as a backfill banner on the interest. * * Nurturing is bidirectional with qualified (deal pauses → reopens), * and can re-enter the EOI path when supply opens up. */ export const STAGE_TRANSITIONS: Record = { // L2: include `nurturing` so a fresh enquiry can be parked straight into // the nurturing column without first round-tripping through `qualified`. enquiry: ['qualified', 'nurturing', 'eoi'], qualified: ['enquiry', 'nurturing', 'eoi'], nurturing: ['qualified', 'eoi'], eoi: ['qualified', 'reservation', 'deposit_paid'], reservation: ['eoi', 'deposit_paid'], deposit_paid: ['reservation', 'contract'], contract: ['deposit_paid'], }; export function canTransitionStage(from: string, to: string): boolean { if (from === to) return true; const fromStage = safeStage(from); const toStage = safeStage(to); return STAGE_TRANSITIONS[fromStage].includes(toStage); } export function safeStage(value: string | null | undefined): PipelineStage { return PIPELINE_STAGES.includes(value as PipelineStage) ? (value as PipelineStage) : 'enquiry'; } export function stageLabel(stage: string | null | undefined): string { return STAGE_LABELS[safeStage(stage)]; } export function stageBadgeClass(stage: string | null | undefined): string { return STAGE_BADGE[safeStage(stage)]; } export function stageDotClass(stage: string | null | undefined): string { return STAGE_DOT[safeStage(stage)]; } // ─── Berth Statuses ────────────────────────────────────────────────────────── export const BERTH_STATUSES = ['available', 'under_offer', 'sold'] as const; export type BerthStatus = (typeof BERTH_STATUSES)[number]; // ─── Berth single-select catalogues (mirror NocoDB) ────────────────────────── // Stored as free text in the DB so legacy values still load, but the form // presents only the canonical options below. export const BERTH_AREAS = ['A', 'B', 'C', 'D', 'E'] as const; export const BERTH_BOW_FACING_OPTIONS = ['North', 'South', 'East', 'West'] as const; export const BERTH_SIDE_PONTOON_OPTIONS = [ 'No', 'Quay SB', 'Quay PT', 'Quay SB, Yes PT', 'Quay PT, Yes SB', 'Yes SB', 'Yes PT', 'Yes SB, PT', 'Finger SB', 'Finger PT', ] as const; export const BERTH_MOORING_TYPES = [ 'Side Pier / Med Mooring', '2x Med Mooring', 'Side Pier / Finger', 'Finger / Med Mooring', '2x Finger', ] as const; export const BERTH_CLEAT_TYPES = ['A3', 'A5'] as const; export const BERTH_CLEAT_CAPACITIES = ['10-14 ton break load', '20-24 ton break load'] as const; export const BERTH_BOLLARD_TYPES = ['Bull bollard type A', 'Bull bollard type B'] as const; export const BERTH_BOLLARD_CAPACITIES = ['20 ton break load', '40 ton break load'] as const; export const BERTH_ACCESS_OPTIONS = [ 'Car to Vessel', 'Car to Quai, Cart to Vessel', 'Cart to Vessel', 'Car (3t) to Vessel', 'Car (3.5t) to Vessel', ] as const; /** * Map a readonly enum tuple into shadcn `