'use client'; import { useState } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Archive, ArrowRight, LayoutList, Plus } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { ColumnPicker } from '@/components/shared/column-picker'; import { DataTable } from '@/components/shared/data-table'; import { EmptyState } from '@/components/shared/empty-state'; import { FilterBar } from '@/components/shared/filter-bar'; import { PageHeader } from '@/components/shared/page-header'; import { SaveViewDialog } from '@/components/shared/save-view-dialog'; import { SavedViewsDropdown } from '@/components/shared/saved-views-dropdown'; import { TableSkeleton } from '@/components/shared/loading-skeleton'; import { useConfirmation } from '@/hooks/use-confirmation'; import { usePaginatedQuery } from '@/hooks/use-paginated-query'; import { usePermissions } from '@/hooks/use-permissions'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { useTablePreferences } from '@/hooks/use-table-preferences'; import { apiFetch } from '@/lib/api/client'; import { ResidentialInterestCard } from './residential-interest-card'; import { getResidentialInterestColumns, RESIDENTIAL_INTEREST_COLUMN_OPTIONS, RESIDENTIAL_INTEREST_DEFAULT_HIDDEN, type ResidentialInterestRow, } from './residential-interest-columns'; import { DEFAULT_RESIDENTIAL_PIPELINE_STAGES, RESIDENTIAL_STAGE_LABELS, residentialInterestFilterDefinitions, } from './residential-interest-filters'; /** * Residential interests list - parity with the main InterestList. Wires * the same DataTable + FilterBar + ColumnPicker + SavedViews + bulkActions * stack onto the /api/v1/residential/interests endpoint. Kanban view is * intentionally omitted (the residential pipeline stages differ and the * board layout isn't yet wired through for them - opens as a follow-up). */ export function ResidentialInterestsList() { const params = useParams<{ portSlug: string }>(); const router = useRouter(); const portSlug = params?.portSlug ?? ''; const queryClient = useQueryClient(); const { confirm, dialog: confirmDialog } = useConfirmation(); const { can } = usePermissions(); const [saveViewOpen, setSaveViewOpen] = useState(false); // Bulk-action dialog state - same shape as the main InterestList so // the inner controls feel identical. const [stageDialog, setStageDialog] = useState<{ ids: string[] } | null>(null); const [stageChoice, setStageChoice] = useState( DEFAULT_RESIDENTIAL_PIPELINE_STAGES[0] ?? 'new', ); const { data, pagination, isLoading, isFetching, sort, setSort, setPage, setPageSize, filters, setFilter, setAllFilters, clearFilters, } = usePaginatedQuery({ queryKey: ['residential-interests'], endpoint: '/api/v1/residential/interests', filterDefinitions: residentialInterestFilterDefinitions, }); useRealtimeInvalidation({ 'residential_interest:created': [['residential-interests']], 'residential_interest:updated': [['residential-interests']], 'residential_interest:archived': [['residential-interests']], }); // Mirror the main list's two-mutation idiom - per-row archive when the // rep uses the kebab on a single row, bulk endpoint when they tick the // checkbox in the header. const archiveMutation = useMutation({ mutationFn: (id: string) => apiFetch(`/api/v1/residential/interests/${id}`, { method: 'DELETE' }), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['residential-interests'] }); }, }); const bulkMutation = useMutation({ mutationFn: async ( payload: | { action: 'archive'; ids: string[] } | { action: 'change_stage'; ids: string[]; pipelineStage: string }, ) => apiFetch<{ data: { summary: { total: number; succeeded: number; failed: number } }; }>('/api/v1/residential/interests/bulk', { method: 'POST', body: payload }), onSuccess: (res) => { void queryClient.invalidateQueries({ queryKey: ['residential-interests'] }); const s = res.data.summary; if (s.failed > 0) { toast.warning( `${s.succeeded} of ${s.total} succeeded. ${s.failed} failed - check the activity log.`, ); } else { toast.success(`Updated ${s.succeeded} interest${s.succeeded === 1 ? '' : 's'}`); } }, }); const columns = getResidentialInterestColumns({ portSlug, onArchive: async (interest) => { const ok = await confirm({ title: 'Archive interest', description: 'This can be undone from the archived list.', confirmLabel: 'Archive', }); if (!ok) return; archiveMutation.mutate(interest.id); }, }); // Persisted per-user column visibility + density. const { hidden, setHidden, density } = useTablePreferences( 'residential_interests', RESIDENTIAL_INTEREST_DEFAULT_HIDDEN, ); const columnVisibility = Object.fromEntries(hidden.map((id) => [id, false])); // Stages enum is set at module load - no runtime invariant to enforce. // (Earlier draft had a useEffect resetting `stageChoice` if it fell // out of the enum; React Compiler flags setState-in-effect, so we // rely on the dropdown's controlled value to enforce validity.) return (
} />
setAllFilters(savedFilters)} />
{isLoading ? ( ) : ( { setPage(p); setPageSize(ps); }} sort={sort} onSortChange={setSort} isLoading={isFetching && !isLoading} getRowId={(row) => row.id} onRowClick={(row) => router.push(`/${portSlug}/residential/interests/${row.id}` as never)} bulkActions={ can('residential_interests', 'edit') ? [ { label: 'Change stage', icon: ArrowRight, onClick: (ids) => { if (ids.length === 0) return; setStageChoice(DEFAULT_RESIDENTIAL_PIPELINE_STAGES[0] ?? 'new'); setStageDialog({ ids }); }, }, ...(can('residential_interests', 'delete') ? [ { label: 'Archive', icon: Archive, variant: 'destructive' as const, onClick: async (ids: string[]) => { if (ids.length === 0) return; const ok = await confirm({ title: `Archive ${ids.length} interest${ids.length === 1 ? '' : 's'}`, description: 'This can be undone from the archived list.', confirmLabel: 'Archive', }); if (!ok) return; bulkMutation.mutate({ action: 'archive', ids }); }, }, ] : []), ] : undefined } cardRender={(row) => ( )} emptyState={ } /> )} {/* Bulk: change stage */} !o && setStageDialog(null)}> Change stage Move {stageDialog?.ids.length ?? 0} interest {stageDialog?.ids.length === 1 ? '' : 's'} to a new pipeline stage.
{confirmDialog} ); }