'use client'; import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useParams, useRouter } from 'next/navigation'; import { formatDistanceToNow } from 'date-fns'; import { toast } from 'sonner'; import { PageHeader } from '@/components/shared/page-header'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { apiFetch } from '@/lib/api/client'; type TriageState = 'open' | 'assigned' | 'converted' | 'dismissed'; type StateFilter = 'inbox' | 'open' | 'assigned' | 'converted' | 'dismissed' | 'all'; interface Submission { id: string; portId: string; submissionId: string; kind: 'berth_inquiry' | 'residence_inquiry' | 'contact_form'; payload: Record | null; legacyNocodbId: string | null; sourceIp: string | null; userAgent: string | null; receivedAt: string; triageState: TriageState; triagedAt: string | null; triagedBy: string | null; } const TRIAGE_BADGE: Record = { open: 'bg-blue-100 text-blue-800', assigned: 'bg-amber-100 text-amber-900', converted: 'bg-emerald-100 text-emerald-800', dismissed: 'bg-slate-100 text-slate-600', }; interface ListResponse { data: Submission[]; pagination: { nextCursor: { receivedAt: string; id: string } | null }; counts: Record; } const KIND_LABELS: Record = { berth_inquiry: 'Berth inquiry', residence_inquiry: 'Residence inquiry', contact_form: 'Contact form', }; const KIND_COLORS: Record = { berth_inquiry: 'bg-blue-100 text-blue-800', residence_inquiry: 'bg-amber-100 text-amber-800', contact_form: 'bg-slate-100 text-slate-800', }; function pickName(payload: Record | null): string { if (!payload) return ''; const candidates = ['name', 'fullName', 'full_name', 'firstName', 'first_name']; for (const k of candidates) { const v = payload[k]; if (typeof v === 'string' && v.trim()) return v.trim(); } return ''; } function pickEmail(payload: Record | null): string { if (!payload) return ''; const v = payload['email']; return typeof v === 'string' ? v : ''; } function pickPhone(payload: Record | null): string { if (!payload) return ''; const v = payload['phone'] ?? payload['phoneNumber'] ?? payload['phone_number']; return typeof v === 'string' ? v : ''; } export function InquiryInbox() { const [kind, setKind] = useState('all'); const [state, setState] = useState('inbox'); const [expanded, setExpanded] = useState(null); const qc = useQueryClient(); const router = useRouter(); const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const { data, isLoading, error } = useQuery({ queryKey: ['inquiry-inbox', kind, state], queryFn: () => { const qs = new URLSearchParams(); if (kind !== 'all') qs.set('kind', kind); qs.set('state', state); return apiFetch(`/api/v1/admin/website-submissions?${qs}`); }, }); const counts = data?.counts ?? {}; const totalAll = useMemo(() => Object.values(counts).reduce((sum, n) => sum + n, 0), [counts]); const rows = data?.data ?? []; const triageMutation = useMutation({ mutationFn: (args: { id: string; state: TriageState }) => apiFetch(`/api/v1/admin/website-submissions/${args.id}/triage`, { method: 'PATCH', body: { state: args.state }, }), onSuccess: (_data, vars) => { qc.invalidateQueries({ queryKey: ['inquiry-inbox'] }); toast.success(`Marked ${vars.state}.`); }, onError: (err: unknown) => { toast.error(err instanceof Error ? err.message : 'Triage update failed'); }, }); function convertToClient(row: Submission) { // Mark converted then jump to /clients with prefilled query params. // The /clients page reads ?prefill_* on mount and opens the New // Client form with the values populated. Final form submission is // entirely operator-driven; this just hands the data over. const name = pickName(row.payload); const email = pickEmail(row.payload); const phone = pickPhone(row.payload); triageMutation.mutate({ id: row.id, state: 'converted' }); const qs = new URLSearchParams(); if (name) qs.set('prefill_name', name); if (email) qs.set('prefill_email', email); if (phone) qs.set('prefill_phone', phone); qs.set('prefill_source', 'website'); qs.set('prefill_inquiry_id', row.id); router.push(`/${portSlug}/clients?${qs.toString()}`); } return (
setKind('all')} /> setKind('berth_inquiry')} /> setKind('residence_inquiry')} /> setKind('contact_form')} />
State: setState('inbox')} /> setState('open')} /> setState('assigned')} /> setState('converted')} /> setState('dismissed')} /> setState('all')} />
{isLoading ? (

Loading…

) : error ? (

Failed to load inquiries: {error instanceof Error ? error.message : 'unknown error'}

) : rows.length === 0 ? ( No website submissions yet for this filter. ) : (
{rows.map((row) => { const name = pickName(row.payload); const email = pickEmail(row.payload); const phone = pickPhone(row.payload); const ago = formatDistanceToNow(new Date(row.receivedAt), { addSuffix: true }); const isOpen = expanded === row.id; return (
{KIND_LABELS[row.kind]} {row.triageState} {ago}
{name || '(no name supplied)'}
{email ? {email} : null} {phone ? {phone} : null} {row.sourceIp ? ( from {row.sourceIp} ) : null}
{row.triageState !== 'converted' && row.triageState !== 'dismissed' && (
{row.triageState === 'open' && ( )}
)} {(row.triageState === 'converted' || row.triageState === 'dismissed') && (
)}
{isOpen && (
                        {JSON.stringify(row.payload, null, 2)}
                      
)}
); })}
)}
); } function FilterChip({ label, active, onClick, }: { label: string; active: boolean; onClick: () => void; }) { return ( ); }