'use client'; import { useParams } from 'next/navigation'; import { useMemo } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { DndContext, closestCenter, type DragEndEvent } from '@dnd-kit/core'; import { PipelineColumn } from '@/components/interests/pipeline-column'; import { apiFetch } from '@/lib/api/client'; import { usePipelineStore } from '@/stores/pipeline-store'; import { PIPELINE_STAGES } from '@/lib/constants'; const STAGE_LABELS: Record = { open: 'Open', details_sent: 'Details Sent', in_communication: 'In Communication', visited: 'Visited', signed_eoi_nda: 'Signed EOI/NDA', deposit_10pct: 'Deposit 10%', contract: 'Contract', completed: 'Completed', }; interface InterestRow { id: string; clientName: string | null; berthMooringNumber: string | null; leadCategory: string | null; pipelineStage: string; updatedAt: string; } export function PipelineBoard() { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const queryClient = useQueryClient(); const { boardFilters } = usePipelineStore(); const { data: allData, isLoading } = useQuery<{ data: InterestRow[] }>({ queryKey: ['interests-board', portSlug], queryFn: () => apiFetch('/api/v1/interests?limit=500'), }); const interests = useMemo(() => { if (!allData?.data) return []; return allData.data.filter((i) => { if (boardFilters.leadCategory && i.leadCategory !== boardFilters.leadCategory) return false; if (boardFilters.search) { const q = boardFilters.search.toLowerCase(); if (!i.clientName?.toLowerCase().includes(q)) return false; } return true; }); }, [allData, boardFilters]); const grouped = useMemo(() => { const map: Record = {}; for (const stage of PIPELINE_STAGES) { map[stage] = []; } for (const interest of interests) { if (map[interest.pipelineStage]) { map[interest.pipelineStage]!.push(interest); } } return map; }, [interests]); async function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over || active.id === over.id) return; // over.id is a stage when dropped on a column, or an item id when dropped on a card let newStage = over.id as string; // If dropped on a card (not a stage), find which stage that card belongs to if (!PIPELINE_STAGES.includes(newStage as typeof PIPELINE_STAGES[number])) { const targetInterest = interests.find((i) => i.id === newStage); if (!targetInterest) return; newStage = targetInterest.pipelineStage; } const interestId = active.id as string; const currentInterest = interests.find((i) => i.id === interestId); if (!currentInterest || currentInterest.pipelineStage === newStage) return; // Optimistic update queryClient.setQueryData<{ data: InterestRow[] }>( ['interests-board', portSlug], (old) => { if (!old) return old; return { ...old, data: old.data.map((i) => i.id === interestId ? { ...i, pipelineStage: newStage } : i, ), }; }, ); try { await apiFetch(`/api/v1/interests/${interestId}/stage`, { method: 'PATCH', body: JSON.stringify({ pipelineStage: newStage }), }); queryClient.invalidateQueries({ queryKey: ['interests'] }); } catch { // Revert optimistic update queryClient.invalidateQueries({ queryKey: ['interests-board', portSlug] }); } } if (isLoading) { return
; } return (
{PIPELINE_STAGES.map((stage) => ( ))}
); }