Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
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<VocabularyKey, VocabularyDef>(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;
|
|
}
|