'use client'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useParams } from 'next/navigation'; import type { DetailTab } from '@/components/shared/detail-layout'; import { InlineEditableField } from '@/components/shared/inline-editable-field'; import { InlineTagEditor } from '@/components/shared/inline-tag-editor'; import { NotesList } from '@/components/shared/notes-list'; import { EntityActivityFeed } from '@/components/shared/entity-activity-feed'; import { ReservationList, type ReservationRow } from '@/components/reservations/reservation-list'; import { RemindersInline } from '@/components/reminders/reminders-inline'; import { YachtOwnershipHistory } from '@/components/yachts/yacht-ownership-history'; import { apiFetch } from '@/lib/api/client'; import { stageLabel } from '@/lib/constants'; type YachtPatchField = | 'name' | 'hullNumber' | 'registration' | 'flag' | 'yearBuilt' | 'builder' | 'model' | 'hullMaterial' | 'lengthFt' | 'widthFt' | 'draftFt' | 'lengthM' | 'widthM' | 'draftM' | 'status' | 'notes'; const STATUS_OPTIONS = [ { value: 'active', label: 'Active' }, { value: 'retired', label: 'Retired' }, { value: 'sold_away', label: 'Sold away' }, ]; interface YachtTabsYacht { id: string; name: string; hullNumber: string | null; registration: string | null; flag: string | null; yearBuilt: number | null; builder: string | null; model: string | null; hullMaterial: string | null; lengthFt: string | null; widthFt: string | null; draftFt: string | null; lengthM: string | null; widthM: string | null; draftM: string | null; status: string; notes: string | null; tags?: Array<{ id: string; name: string; color: string }>; } interface YachtTabsOptions { yachtId: string; currentUserId?: string; yacht: YachtTabsYacht; } function useYachtPatch(yachtId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: async (patch: Partial>) => apiFetch(`/api/v1/yachts/${yachtId}`, { method: 'PATCH', body: patch, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['yachts', yachtId] }); }, }); } function EditableRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function OverviewTab({ yachtId, yacht, currentUserId, }: { yachtId: string; yacht: YachtTabsYacht; currentUserId?: string; }) { const mutation = useYachtPatch(yachtId); const save = (field: YachtPatchField, transform?: (v: string | null) => string | number | null) => async (next: string | null) => { const value = transform ? transform(next) : next; await mutation.mutateAsync({ [field]: value }); }; /** * Bidirectional dimension save: when the rep edits Length/Width/Draft * in feet, also write the metric counterpart (and vice versa). Avoids * the "I entered ft but the m row still says '-'" surprise. * * If the rep clears a field (next === null), only that side is * cleared — we never overwrite their other-unit value with a derived * one, since they may have intentionally entered a more precise * metric figure. */ function saveDimension( primaryField: 'lengthFt' | 'widthFt' | 'draftFt' | 'lengthM' | 'widthM' | 'draftM', ) { const isFt = primaryField.endsWith('Ft'); const counterpart = ( isFt ? primaryField.replace('Ft', 'M') : primaryField.replace('M', 'Ft') ) as YachtPatchField; return async (next: string | null) => { if (next === null || next === '') { await mutation.mutateAsync({ [primaryField]: null }); return; } const n = Number.parseFloat(next); if (!Number.isFinite(n)) { await mutation.mutateAsync({ [primaryField]: next }); return; } const FT_PER_M = 3.28084; const converted = isFt ? n / FT_PER_M : n * FT_PER_M; const convertedStr = converted .toFixed(2) .replace(/\.0+$/, '') .replace(/(\.\d)0$/, '$1'); await mutation.mutateAsync({ [primaryField]: next, [counterpart]: convertedStr, }); }; } const yearTransform = (next: string | null) => { if (next === null) return null; const n = Number.parseInt(next, 10); return Number.isNaN(n) ? null : n; }; return (
{/* Identity */}

Identity

{/* Build */}

Build

{/* Dimensions (ft) */}

Dimensions (ft)

{/* Dimensions (m) */}

Dimensions (m)

{/* Notes — threaded list (parity with clients/interests/companies). The legacy single-field `yacht.notes` column stays in schema for the EOI/contract merge-field path; OverviewTab no longer exposes it for editing here. */}

Notes

); } function YachtInterestsTab({ yachtId }: { yachtId: string }) { const { data, isLoading } = useQuery<{ data: Array<{ id: string; pipelineStage: string; clientName: string | null; berthMooringNumber: string | null; updatedAt: string; }>; }>({ queryKey: ['interests', 'by-yacht', yachtId], queryFn: () => apiFetch(`/api/v1/interests?yachtId=${yachtId}&limit=50&order=desc`), }); const interests = data?.data ?? []; if (isLoading) return

Loading…

; if (interests.length === 0) { return (

No interests linked to this yacht

Interests for this yacht will appear here once a sales rep links them. Add an interest from the Interests tab on the owner’s client page.

); } return ( ); } function YachtReservationsTab({ yachtId }: { yachtId: string }) { const routeParams = useParams<{ portSlug: string }>(); const portSlug = routeParams?.portSlug ?? ''; const { data, isLoading } = useQuery<{ data: ReservationRow[] }>({ queryKey: ['berth-reservations', 'by-yacht', yachtId], queryFn: () => apiFetch(`/api/v1/berth-reservations?yachtId=${yachtId}&limit=50&order=desc`), }); if (isLoading) return

Loading…

; return ( ); } export function getYachtTabs({ yachtId, currentUserId, yacht }: YachtTabsOptions): DetailTab[] { return [ { id: 'overview', label: 'Overview', content: , }, { id: 'ownership-history', label: 'Ownership History', content: , }, { id: 'interests', label: 'Interests', content: , }, { id: 'reservations', label: 'Reservations', content: , }, { id: 'notes', label: 'Notes', content: ( ), }, { id: 'activity', label: 'Activity', content: ( ), }, ]; }