Files
pn-new-crm/src/lib/constants.ts
Matt Ciaccio e40b6c3d99 feat(berths): full NocoDB field parity, numeric types, sales edit access
Aligns the berths schema with the 117 production rows in NocoDB and exposes
every field for editing via the BerthForm sheet.

Schema (migration 0020):
- power_capacity / voltage / nominal_boat_size / nominal_boat_size_m: text -> numeric
  (NocoDB stores plain numbers; text was wrong shape and broke filter/sort)
- ADD status_override_mode text (1/117 legacy rows have a value; carried
  forward for parity but not yet wired into the UI)
- USING NULLIF(TRIM(...), '')::numeric so legacy whitespace and empty
  strings convert cleanly

Validator + service:
- updateBerthSchema / createBerthSchema use z.coerce.number() for the
  four numeric fields
- berths.service stringifies numeric values for Drizzle's numeric type

Form (src/components/berths/berth-form.tsx):
- adds: nominal boat size (ft/m), water depth (ft/m) + "is minimum" flag,
  side pontoon, cleat type/capacity, bollard type/capacity, bow facing
- converts to typed selects (with NocoDB option lists in src/lib/constants):
  area, side pontoon, mooring type, cleat type/capacity, bollard type/capacity,
  access
- power capacity / voltage become numeric inputs (with kW / V hints)

Permissions (seed.ts + dev DB):
- sales_manager and sales_agent: berths.edit false -> true
  ("sales will sometimes have to update these and I cannot be the only one")
- super_admin / director already had it; viewer stays read-only
- dev DB updated in-place via UPDATE roles ... jsonb_set

Verification:
- pnpm exec vitest run: 858/858 passing
- pnpm exec tsc --noEmit: same 36 errors as baseline (all pre-existing
  on feat/mobile-foundation, none introduced)
- lint clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:30:32 +02:00

266 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ─── Pipeline Stages ─────────────────────────────────────────────────────────
export const PIPELINE_STAGES = [
'open',
'details_sent',
'in_communication',
'eoi_sent',
'eoi_signed',
'deposit_10pct',
'contract_sent',
'contract_signed',
'completed',
] as const;
export type PipelineStage = (typeof PIPELINE_STAGES)[number];
export const STAGE_LABELS: Record<PipelineStage, string> = {
open: 'Open',
details_sent: 'Details Sent',
in_communication: 'In Comms',
eoi_sent: 'EOI Sent',
eoi_signed: 'EOI Signed',
deposit_10pct: 'Deposit 10%',
contract_sent: 'Contract Sent',
contract_signed: 'Contract Signed',
completed: 'Completed',
};
// Compact labels for cramped contexts (mobile chart axes, dense tables).
export const STAGE_SHORT_LABELS: Record<PipelineStage, string> = {
open: 'Open',
details_sent: 'Details',
in_communication: 'Comms',
eoi_sent: 'EOI →',
eoi_signed: 'EOI ✓',
deposit_10pct: 'Dep.',
contract_sent: 'Ctr →',
contract_signed: 'Ctr ✓',
completed: 'Done',
};
export const STAGE_BADGE: Record<PipelineStage, string> = {
open: 'bg-slate-100 text-slate-700',
details_sent: 'bg-blue-100 text-blue-700',
in_communication: 'bg-sky-100 text-sky-700',
eoi_sent: 'bg-indigo-100 text-indigo-700',
eoi_signed: 'bg-amber-100 text-amber-700',
deposit_10pct: 'bg-orange-100 text-orange-700',
contract_sent: 'bg-yellow-100 text-yellow-700',
contract_signed: 'bg-green-100 text-green-700',
completed: 'bg-emerald-100 text-emerald-700',
};
export const STAGE_DOT: Record<PipelineStage, string> = {
open: 'bg-slate-400',
details_sent: 'bg-blue-500',
in_communication: 'bg-sky-500',
eoi_sent: 'bg-indigo-500',
eoi_signed: 'bg-amber-500',
deposit_10pct: 'bg-orange-500',
contract_sent: 'bg-yellow-500',
contract_signed: 'bg-green-500',
completed: 'bg-emerald-500',
};
// Default revenue-forecast probability weights per stage (01).
// Editable per port via settings (`pipeline_weights`); these are the fallbacks.
export const STAGE_WEIGHTS: Record<PipelineStage, number> = {
open: 0.05,
details_sent: 0.1,
in_communication: 0.2,
eoi_sent: 0.4,
eoi_signed: 0.6,
deposit_10pct: 0.75,
contract_sent: 0.85,
contract_signed: 0.95,
completed: 1.0,
};
// Allowed transitions out of each stage. Used by changeInterestStage to guard
// against accidental skips (e.g. dragging a card from Completed back to Open,
// or jumping Open straight to Completed). Forward moves of 1-2 stages are
// permitted; backward moves are limited to the immediate predecessor unless
// the lifecycle (EOI/contract chain) needs an explicit rewind.
export const STAGE_TRANSITIONS: Record<PipelineStage, readonly PipelineStage[]> = {
open: ['details_sent', 'in_communication', 'eoi_sent', 'eoi_signed'],
details_sent: ['open', 'in_communication', 'eoi_sent', 'eoi_signed'],
in_communication: ['open', 'details_sent', 'eoi_sent', 'eoi_signed'],
eoi_sent: ['in_communication', 'eoi_signed', 'deposit_10pct'],
eoi_signed: ['eoi_sent', 'deposit_10pct', 'contract_sent', 'contract_signed'],
deposit_10pct: ['eoi_signed', 'contract_sent', 'contract_signed'],
contract_sent: ['eoi_signed', 'deposit_10pct', 'contract_signed'],
contract_signed: ['contract_sent', 'deposit_10pct', 'completed'],
completed: ['contract_signed'],
};
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) : 'open';
}
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_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;
// ─── Lead Categories ─────────────────────────────────────────────────────────
export const LEAD_CATEGORIES = ['general_interest', 'specific_qualified', 'hot_lead'] as const;
export type LeadCategory = (typeof LEAD_CATEGORIES)[number];
// ─── Document Types ──────────────────────────────────────────────────────────
export const DOCUMENT_TYPES = ['eoi', 'contract', 'nda', 'reservation_agreement', 'other'] as const;
export type DocumentType = (typeof DOCUMENT_TYPES)[number];
// ─── Document Statuses ───────────────────────────────────────────────────────
export const DOCUMENT_STATUSES = [
'draft',
'sent',
'partially_signed',
'completed',
'expired',
'cancelled',
] as const;
export type DocumentStatus = (typeof DOCUMENT_STATUSES)[number];
// ─── Expense Categories ──────────────────────────────────────────────────────
export const EXPENSE_CATEGORIES = [
'fuel',
'maintenance',
'cleaning',
'docking',
'insurance',
'utilities',
'marina_fees',
'repairs',
'equipment',
'crew',
'administration',
'marketing',
'travel',
'entertainment',
'other',
] as const;
export type ExpenseCategory = (typeof EXPENSE_CATEGORIES)[number];
// ─── Payment Methods ─────────────────────────────────────────────────────────
export const PAYMENT_METHODS = [
'bank_transfer',
'credit_card',
'debit_card',
'cash',
'cheque',
'crypto',
'other',
] as const;
export type PaymentMethod = (typeof PAYMENT_METHODS)[number];
// ─── Notification Types ──────────────────────────────────────────────────────
export const NOTIFICATION_TYPES = [
// Interest / pipeline
'interest_stage_changed',
'interest_created',
'interest_assigned',
// Documents
'document_sent',
'document_signed',
'document_completed',
'document_expired',
'document_reminder',
// Reminders
'reminder_due',
'reminder_overdue',
'reminder_assigned',
// Financial
'invoice_sent',
'invoice_paid',
'invoice_overdue',
// Notes
'mention',
// Email
'email_received',
// System
'system_alert',
'job_failed',
'bulk_operation_complete',
'export_ready',
// Berths
'berth_status_changed',
'berth_waiting_list_update',
] as const;
export type NotificationType = (typeof NOTIFICATION_TYPES)[number];