'use client'; import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { DndContext, closestCenter, type DragEndEvent, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { GripVertical, Plus, Loader2, Trash2, Users } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { EmptyState } from '@/components/shared/empty-state'; import { ClientPicker } from '@/components/shared/client-picker'; import { usePermissions } from '@/hooks/use-permissions'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; interface WaitingListEntry { id: string; clientId: string; clientName: string | null; position: number; priority: string; notifyPref: string; notes: string | null; createdAt: string; } interface WaitingListManagerProps { berthId: string; } /** Shape the PUT (full-replace) endpoint accepts per entry. */ function toPutEntry(e: WaitingListEntry, position: number) { return { clientId: e.clientId, position, priority: e.priority as 'normal' | 'high', notifyPref: e.notifyPref as 'email' | 'in_app' | 'both', notes: e.notes ?? undefined, }; } function SortableEntry({ entry, isNext, canManage, onRemove, }: { entry: WaitingListEntry; isNext: boolean; canManage: boolean; onRemove: (id: string) => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: entry.id, disabled: !canManage, }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (
{canManage ? ( ) : null} {entry.position}

{entry.clientName ?? `Client ${entry.clientId.slice(0, 8)}`}

{entry.notes &&

{entry.notes}

}
{isNext ? ( Next in line ) : null} {entry.priority === 'high' ? High : null} {canManage ? ( ) : null}
); } export function WaitingListManager({ berthId }: WaitingListManagerProps) { const queryClient = useQueryClient(); const { can } = usePermissions(); const canManage = can('berths', 'manage_waiting_list'); const sensors = useSensors(useSensor(PointerSensor)); const [showAddForm, setShowAddForm] = useState(false); const [newClientId, setNewClientId] = useState(null); const [newPriority, setNewPriority] = useState<'normal' | 'high'>('normal'); const [newNotes, setNewNotes] = useState(''); const { data, isLoading } = useQuery<{ data: WaitingListEntry[] }>({ queryKey: ['berth-waiting-list', berthId], queryFn: () => apiFetch(`/api/v1/berths/${berthId}/waiting-list`), }); useRealtimeInvalidation({ 'berth:waitingListChanged': [['berth-waiting-list', berthId]], }); const entries = data?.data ?? []; function resetForm() { setShowAddForm(false); setNewClientId(null); setNewPriority('normal'); setNewNotes(''); } const reorderMutation = useMutation({ mutationFn: (body: { entryId: string; newPosition: number }) => apiFetch(`/api/v1/berths/${berthId}/waiting-list`, { method: 'PATCH', body }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['berth-waiting-list', berthId] }); }, onError: (err) => toastError(err), }); const replaceMutation = useMutation({ mutationFn: (entries: ReturnType[]) => apiFetch(`/api/v1/berths/${berthId}/waiting-list`, { method: 'PUT', body: { entries }, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['berth-waiting-list', berthId] }); resetForm(); }, onError: (err) => toastError(err), }); function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over || active.id === over.id) return; const overEntry = entries.find((e) => e.id === over.id); if (!overEntry) return; reorderMutation.mutate({ entryId: active.id as string, newPosition: overEntry.position }); } function handleAdd() { if (!newClientId) return; if (entries.some((e) => e.clientId === newClientId)) { toast.error('That client is already on this waiting list.'); return; } const next = [ ...entries.map((e, i) => toPutEntry(e, i + 1)), { clientId: newClientId, position: entries.length + 1, priority: newPriority, notifyPref: 'email' as const, notes: newNotes.trim() || undefined, }, ]; replaceMutation.mutate(next); } function handleRemove(entryId: string) { const remaining = entries.filter((e) => e.id !== entryId).map((e, i) => toPutEntry(e, i + 1)); replaceMutation.mutate(remaining); } return (

Waiting list

When this berth becomes available, the person at the top is flagged to the team.

{canManage ? ( ) : null}
{showAddForm && canManage && (
setNewNotes(e.target.value)} />
)} {isLoading ? (
) : entries.length === 0 ? ( ) : ( e.id)} strategy={verticalListSortingStrategy}>
{entries.map((entry, i) => ( ))}
)}
); }