'use client'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { type DetailTab } from '@/components/shared/detail-layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { EntityActivityFeed } from '@/components/shared/entity-activity-feed'; import { InlineEditableField } from '@/components/shared/inline-editable-field'; import { InlineTagEditor } from '@/components/shared/inline-tag-editor'; import { apiFetch } from '@/lib/api/client'; import { BerthReservationsTab } from './berth-reservations-tab'; import { BerthInterestsTab } from './berth-interests-tab'; import { BerthInterestPulse } from './berth-interest-pulse'; import { BerthDocumentsTab } from './berth-documents-tab'; type BerthData = { id: string; mooringNumber: string; area: string | null; status: string; lengthFt: string | null; lengthM: string | null; widthFt: string | null; widthM: string | null; draftFt: string | null; draftM: string | null; widthIsMinimum: boolean | null; nominalBoatSize: string | null; nominalBoatSizeM: string | null; waterDepth: string | null; waterDepthM: string | null; waterDepthIsMinimum: boolean | null; sidePontoon: string | null; powerCapacity: string | null; voltage: string | null; mooringType: string | null; cleatType: string | null; cleatCapacity: string | null; bollardType: string | null; bollardCapacity: string | null; access: string | null; price: string | null; priceCurrency: string; bowFacing: string | null; berthApproved: boolean | null; tenureType: string; tenureYears: number | null; tenureStartDate: string | null; tenureEndDate: string | null; statusLastChangedReason: string | null; statusLastModified: string | null; tags: Array<{ id: string; name: string; color: string }>; }; function SpecRow({ label, value }: { label: string; value: React.ReactNode }) { if (!value && value !== 0 && value !== false) return null; // Mobile-first: stack vertically with label on top so long values // (e.g. "206.69 ft / 62.99 m") never clip at the right edge. // From `sm` (>=640px) up: switch to the original two-column layout. return (
{label} {value}
); } function useBerthPatch(berthId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: async (patch: Record) => apiFetch(`/api/v1/berths/${berthId}`, { method: 'PATCH', body: patch, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['berths', berthId] }); qc.invalidateQueries({ queryKey: ['berths'] }); }, }); } /** * Editable spec row. Wraps SpecRow with InlineEditableField for fields * the operator commonly tweaks (length, width, draft, side pontoon, etc). * Read-only fields (mooringNumber, area) keep using plain SpecRow. * * Numeric fields are stored as strings in the schema (postgres NUMERIC); * the `numeric` flag tells us to parse before sending and display "-" when * blank. */ function EditableSpec({ label, value, field, patch, numeric = false, suffix, }: { label: string; value: string | null; field: string; patch: ReturnType; numeric?: boolean; suffix?: string; }) { return (
{label} { if (numeric) { if (next === null || next.trim() === '') { await patch.mutateAsync({ [field]: null }); return; } const n = Number.parseFloat(next); if (Number.isNaN(n)) throw new Error('Must be a number'); await patch.mutateAsync({ [field]: n }); return; } await patch.mutateAsync({ [field]: next }); }} placeholder={suffix ? `e.g. 25${suffix ? ` ${suffix}` : ''}` : undefined} />
); } function OverviewTab({ berth }: { berth: BerthData }) { const patch = useBerthPatch(berth.id); // Round to at most 2 decimals; trim trailing zeros so "5.00" -> "5". const fmt = (v: string | null, fractionDigits = 2): string | null => { if (v == null || v === '') return null; const n = Number(v); if (Number.isNaN(n)) return v; return n.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: fractionDigits, }); }; // Read-only display helper for the metric column on dimensions — // mirrors the pre-edit "X ft / Y m" rendering for fields where only // the foot value is editable today. const formatNominalBoatSize = (ft: string | null, m: string | null): string | null => { const ftFmt = fmt(ft, 0); const mFmt = fmt(m); const parts: string[] = []; if (ftFmt) parts.push(`${ftFmt} ft`); if (mFmt) parts.push(`${mFmt} m`); return parts.length > 0 ? parts.join(' / ') : null; }; return (
{/* Sales pulse - top-of-page so reps doing berth-level triage can see who's interested + how warm without clicking into the Interests tab. */}
{/* Specifications */} Specifications {/* Infrastructure & Pricing */}
Infrastructure Tenure & Pricing {berth.tenureType === 'fixed_term' && ( <> )} Tags
); } function StubTab({ label }: { label: string }) { return (

{label} coming soon

); } export function buildBerthTabs(berth: BerthData): DetailTab[] { return [ { id: 'overview', label: 'Overview', content: , }, { id: 'interests', label: 'Interests', content: , }, { id: 'reservations', label: 'Reservations', content: , }, { id: 'documents', label: 'Documents', content: , }, { id: 'waiting-list', label: 'Waiting List', content: , }, { id: 'maintenance', label: 'Maintenance Log', content: , }, { id: 'activity', label: 'Activity', content: ( ), }, ]; }