'use client'; import { useState } from 'react'; import Link from 'next/link'; import type { Route } from 'next'; import { useParams, useRouter } from 'next/navigation'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Plus } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { PageHeader } from '@/components/shared/page-header'; import { CountryCombobox } from '@/components/shared/country-combobox'; import { TimezoneCombobox } from '@/components/shared/timezone-combobox'; import { SubdivisionCombobox } from '@/components/shared/subdivision-combobox'; import { PhoneInput, type PhoneInputValue } from '@/components/shared/phone-input'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { cn } from '@/lib/utils'; import type { CountryCode } from '@/lib/i18n/countries'; import { DataTable } from '@/components/shared/data-table'; import { ColumnPicker } from '@/components/shared/column-picker'; import { useTablePreferences } from '@/hooks/use-table-preferences'; import { getResidentialClientColumns, RESIDENTIAL_CLIENT_COLUMN_OPTIONS, RESIDENTIAL_CLIENT_DEFAULT_HIDDEN, type ResidentialClientRow, } from '@/components/residential/residential-client-columns'; interface ListResponse { data: ResidentialClientRow[]; pagination: { total: number; page: number; pageSize: number }; } const STATUS_LABELS: Record = { prospect: 'Prospect', active: 'Active', inactive: 'Inactive', }; export function ResidentialClientsList() { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const router = useRouter(); const [createOpen, setCreateOpen] = useState(false); const [search, setSearch] = useState(''); const { data, isLoading } = useQuery({ queryKey: ['residential-clients', { search }], queryFn: () => { const qs = new URLSearchParams({ search, limit: '50' }); return apiFetch(`/api/v1/residential/clients?${qs.toString()}`); }, }); useRealtimeInvalidation({ 'residential_client:created': [['residential-clients']], 'residential_client:updated': [['residential-clients']], 'residential_client:archived': [['residential-clients']], 'residential_client:restored': [['residential-clients']], }); const columns = getResidentialClientColumns({ portSlug }); // Per-user column visibility, persisted via /api/v1/me — same hook + UX as // the marina clients/interests lists. "Residence" is hidden by default // (it's empty for nearly every residential client); "Date added" is shown. const { hidden, setHidden } = useTablePreferences( 'residential-clients', RESIDENTIAL_CLIENT_DEFAULT_HIDDEN, ); const columnVisibility = Object.fromEntries(hidden.map((id) => [id, false])); const rows = data?.data ?? []; return (
setCreateOpen(true)}> New } />
setSearch(e.target.value)} className="max-w-sm" />
r.id} onRowClick={(r) => router.push(`/${portSlug}/residential/clients/${r.id}` as Route)} emptyState={
No residential clients yet.
} cardRender={(row) => } />
); } /** * Mobile card for a residential client — DataTable swaps to this below the * md breakpoint. Self-navigating `` (DataTable's onRowClick only wires * the desktop table rows). Mirrors the marina-side card density. */ function ResidentialClientCard({ portSlug, client, }: { portSlug: string; client: ResidentialClientRow; }) { return (

{client.fullName}

{STATUS_LABELS[client.status] ?? client.status}
{client.email ? {client.email} : null} {client.phone ? {client.phone} : null} {client.placeOfResidence ? {client.placeOfResidence} : null} {client.source ? · {client.source} : null}
); } function NewResidentialClientSheet({ open, onOpenChange, }: { open: boolean; onOpenChange: (v: boolean) => void; }) { const qc = useQueryClient(); const [fullName, setFullName] = useState(''); const [email, setEmail] = useState(''); const [phone, setPhone] = useState(null); const [nationalityIso, setNationalityIso] = useState(null); const [timezone, setTimezone] = useState(null); const [placeOfResidence, setPlaceOfResidence] = useState(''); const [residenceCountry, setResidenceCountry] = useState(null); const [residenceSubdivision, setResidenceSubdivision] = useState(null); const [notes, setNotes] = useState(''); function reset() { setFullName(''); setEmail(''); setPhone(null); setNationalityIso(null); setTimezone(null); setPlaceOfResidence(''); setResidenceCountry(null); setResidenceSubdivision(null); setNotes(''); } const create = useMutation({ mutationFn: () => apiFetch('/api/v1/residential/clients', { method: 'POST', body: { fullName, email: email || undefined, phone: phone?.e164 ?? undefined, phoneE164: phone?.e164 ?? undefined, phoneCountry: phone?.country ?? undefined, nationalityIso: nationalityIso ?? undefined, timezone: timezone ?? undefined, placeOfResidence: placeOfResidence || undefined, placeOfResidenceCountryIso: residenceCountry ?? undefined, subdivisionIso: residenceSubdivision ?? undefined, notes: notes || undefined, source: 'manual', }, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['residential-clients'] }); onOpenChange(false); reset(); toast.success('Residential client added'); }, onError: (err) => { toastError(err); }, }); return ( New residential client
{ e.preventDefault(); create.mutate(); }} >
setFullName(e.target.value)} required />
setEmail(e.target.value)} />
setPlaceOfResidence(e.target.value)} placeholder="City or area" />
{ setResidenceCountry(iso); // Wipe subdivision when country flips - codes are scoped per country. setResidenceSubdivision(null); }} data-testid="rc-residence-country" />
setNotes(e.target.value)} />
); }