import { BERTH_ACCESS_OPTIONS, BERTH_BOLLARD_TYPES, BERTH_CLEAT_TYPES, BERTH_MOORING_TYPES, BERTH_SIDE_PONTOON_OPTIONS, DOCUMENT_TYPES, EXPENSE_CATEGORIES, } from '@/lib/constants'; /** * Per-port vocabularies — pick lists that the rep team needs to be * able to tweak without a code change. Each vocabulary is a JSON * array of strings stored in `system_settings` keyed by * `(port_id, key)`. The defaults below are the values the CRM * shipped with; the admin Vocabularies page lets a port admin * override any of them. * * Consumers should call {@link getVocabulary} to fetch the * effective list (port override first, falling back to default). * Never hardcode a vocabulary in component code — always read * through this module so the admin page is the single source of * truth. */ export type VocabularyKey = | 'interest_temperature_levels' | 'berth_status_change_reasons' | 'berth_tenure_types' | 'expense_categories' | 'document_types' | 'interest_outcome_statuses' | 'berth_side_pontoon_options' | 'berth_mooring_types' | 'berth_cleat_types' | 'berth_bollard_types' | 'berth_access_options'; export interface VocabularyDef { key: VocabularyKey; label: string; description: string; /** Domain grouping for the admin page card layout. */ domain: 'Interests' | 'Berths' | 'Expenses' | 'Documents'; /** Default values shipped with the CRM. */ defaults: readonly string[]; } export const VOCABULARIES: VocabularyDef[] = [ // ─── Interests ───────────────────────────────────────────────────────────── { key: 'interest_temperature_levels', label: 'Interest temperatures', description: 'Heat-level pills shown on each interest card. The first entry is treated as the highest urgency in lists and reports.', domain: 'Interests', defaults: ['HOT', 'WARM', 'COLD'], }, { key: 'interest_outcome_statuses', label: 'Interest outcomes', description: 'Terminal outcome labels recorded when an interest is closed. Used in reports and the lost-deal heat scorer.', domain: 'Interests', defaults: ['won', 'lost_other_marina', 'lost_unqualified', 'lost_no_response', 'cancelled'], }, // ─── Berths ──────────────────────────────────────────────────────────────── { key: 'berth_status_change_reasons', label: 'Berth status-change reasons', description: 'Quick-pick chips shown in the status-change dialog when a berth is moved to Sold / Under Offer.', domain: 'Berths', defaults: [ 'Sold to existing prospect', 'Sold to walk-in', 'Reserved pending deposit', 'Returned to inventory', 'Owner withdrew', ], }, { key: 'berth_tenure_types', label: 'Berth tenure types', description: 'Ownership / lease structure displayed on berth detail.', domain: 'Berths', defaults: ['permanent', 'fixed_term'], }, { key: 'berth_side_pontoon_options', label: 'Side-pontoon configurations', description: 'Options for the Side Pontoon dropdown on the berth form.', domain: 'Berths', defaults: [...BERTH_SIDE_PONTOON_OPTIONS], }, { key: 'berth_mooring_types', label: 'Mooring types', description: 'Mooring-type options on the berth form.', domain: 'Berths', defaults: [...BERTH_MOORING_TYPES], }, { key: 'berth_cleat_types', label: 'Cleat types', description: 'Hardware tiers used in the berth spec form.', domain: 'Berths', defaults: [...BERTH_CLEAT_TYPES], }, { key: 'berth_bollard_types', label: 'Bollard types', description: 'Bollard hardware options on the berth spec form.', domain: 'Berths', defaults: [...BERTH_BOLLARD_TYPES], }, { key: 'berth_access_options', label: 'Access modes', description: 'Vehicle access options shown on the berth spec form.', domain: 'Berths', defaults: [...BERTH_ACCESS_OPTIONS], }, // ─── Expenses ────────────────────────────────────────────────────────────── { key: 'expense_categories', label: 'Expense categories', description: 'Category dropdown on the expense form. Drives the expense PDF group-by-category.', domain: 'Expenses', defaults: [...EXPENSE_CATEGORIES], }, // ─── Documents ───────────────────────────────────────────────────────────── { key: 'document_types', label: 'Document types', description: 'Type tag applied to uploaded documents and template metadata.', domain: 'Documents', defaults: [...DOCUMENT_TYPES], }, ]; const VOCAB_BY_KEY = new Map(VOCABULARIES.map((v) => [v.key, v])); export function getVocabularyDef(key: VocabularyKey): VocabularyDef { const def = VOCAB_BY_KEY.get(key); if (!def) throw new Error(`Unknown vocabulary key: ${key}`); return def; } /** * Resolve the effective vocabulary for a port — merges the port's * override (when present) with the shipped defaults. Falls back to * defaults when the system_settings value is missing or malformed. */ export function resolveVocabulary(key: VocabularyKey, override: unknown): readonly string[] { const def = getVocabularyDef(key); if (!Array.isArray(override)) return def.defaults; const cleaned = override.filter((v): v is string => typeof v === 'string' && v.trim().length > 0); return cleaned.length > 0 ? cleaned : def.defaults; }