diff --git a/src/components/interests/interest-columns.tsx b/src/components/interests/interest-columns.tsx index 3f523d8..81ce165 100644 --- a/src/components/interests/interest-columns.tsx +++ b/src/components/interests/interest-columns.tsx @@ -37,10 +37,34 @@ export interface InterestRow { dateDepositReceived?: string | null; eoiStatus?: string | null; outcome?: string | null; + /** Imperial; nullable. Recommender treats nulls as "no constraint" on + * that axis. Rendered as a compact "60×18×6 ft" string in the list. */ + desiredLengthFt?: string | number | null; + desiredWidthFt?: string | number | null; + desiredDraftFt?: string | number | null; notesCount?: number; tags?: Array<{ id: string; name: string; color: string }>; } +function formatDim(value: string | number | null | undefined): string { + if (value === null || value === undefined || value === '') return '?'; + const n = typeof value === 'number' ? value : parseFloat(value); + if (!Number.isFinite(n)) return '?'; + return Number.isInteger(n) ? String(n) : n.toFixed(1); +} + +function formatDesiredSize(row: InterestRow): string | null { + const { desiredLengthFt, desiredWidthFt, desiredDraftFt } = row; + if ( + (desiredLengthFt === null || desiredLengthFt === undefined || desiredLengthFt === '') && + (desiredWidthFt === null || desiredWidthFt === undefined || desiredWidthFt === '') && + (desiredDraftFt === null || desiredDraftFt === undefined || desiredDraftFt === '') + ) { + return null; + } + return `${formatDim(desiredLengthFt)}×${formatDim(desiredWidthFt)}×${formatDim(desiredDraftFt)} ft`; +} + const SOURCE_LABELS: Record = { website: 'Website', manual: 'Manual', @@ -134,6 +158,16 @@ export function getInterestColumns({ ); }, }, + { + id: 'desiredSize', + header: 'Berth size desired', + enableSorting: false, + cell: ({ row }) => { + const label = formatDesiredSize(row.original); + if (!label) return -; + return {label}; + }, + }, { id: 'pipelineStage', accessorKey: 'pipelineStage', diff --git a/src/components/interests/interest-form.tsx b/src/components/interests/interest-form.tsx index 56c0723..6762c23 100644 --- a/src/components/interests/interest-form.tsx +++ b/src/components/interests/interest-form.tsx @@ -66,6 +66,9 @@ interface InterestFormProps { reminderEnabled?: boolean; reminderDays?: number | null; tags?: Array<{ id: string }>; + desiredLengthFt?: string | number | null; + desiredWidthFt?: string | number | null; + desiredDraftFt?: string | number | null; }; } @@ -131,6 +134,18 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }: reminderEnabled: interest.reminderEnabled ?? false, reminderDays: interest.reminderDays ?? undefined, tagIds: interest.tags?.map((t) => t.id) ?? [], + desiredLengthFt: + interest.desiredLengthFt === null || interest.desiredLengthFt === undefined + ? undefined + : String(interest.desiredLengthFt), + desiredWidthFt: + interest.desiredWidthFt === null || interest.desiredWidthFt === undefined + ? undefined + : String(interest.desiredWidthFt), + desiredDraftFt: + interest.desiredDraftFt === null || interest.desiredDraftFt === undefined + ? undefined + : String(interest.desiredDraftFt), }); } else if (!interest && open) { reset({ @@ -394,6 +409,54 @@ export function InterestForm({ open, onOpenChange, defaultClientId, interest }: + {/* Desired berth dimensions (recommender inputs) */} +
+

+ Berth size desired +

+

+ Imperial. Optional - the recommender treats blank fields as no constraint on that + axis. +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + {/* Notes */}
diff --git a/src/lib/validators/interests.ts b/src/lib/validators/interests.ts index acbe34b..e23f400 100644 --- a/src/lib/validators/interests.ts +++ b/src/lib/validators/interests.ts @@ -10,6 +10,22 @@ import { // ─── Create ────────────────────────────────────────────────────────────────── +/** + * Desired-dimension input. Strings/numbers are coerced to a positive + * decimal (string-typed for postgres `numeric` column compatibility); + * empty strings collapse to `undefined` so a blank form field doesn't + * round-trip "" → numeric error on the API. + */ +const optionalDesiredDimSchema = z + .union([z.string(), z.number()]) + .optional() + .transform((v) => { + if (v === undefined || v === null || v === '') return undefined; + const n = typeof v === 'number' ? v : parseFloat(v); + if (!Number.isFinite(n) || n <= 0) return undefined; + return String(Math.round(n * 100) / 100); + }); + export const createInterestSchema = z.object({ clientId: z.string().min(1), yachtId: z.string().optional(), @@ -21,6 +37,9 @@ export const createInterestSchema = z.object({ tagIds: z.array(z.string()).optional().default([]), reminderEnabled: z.boolean().optional().default(false), reminderDays: z.number().int().min(1).optional(), + desiredLengthFt: optionalDesiredDimSchema, + desiredWidthFt: optionalDesiredDimSchema, + desiredDraftFt: optionalDesiredDimSchema, }); // ─── Update ──────────────────────────────────────────────────────────────────