'use client'; import { useMemo, useState } from 'react'; import { useParams } from 'next/navigation'; import { Plus, Download, FileText, FileSpreadsheet } from 'lucide-react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { DataTable } from '@/components/shared/data-table'; import { FilterBar } from '@/components/shared/filter-bar'; import { PageHeader } from '@/components/shared/page-header'; import { EmptyState } from '@/components/shared/empty-state'; import { TableSkeleton } from '@/components/shared/loading-skeleton'; import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog'; import { PermissionGate } from '@/components/shared/permission-gate'; import { ExpenseFormDialog } from '@/components/expenses/expense-form-dialog'; import { ExpenseCard } from '@/components/expenses/expense-card'; import { buildExpenseFilterDefinitions } from '@/components/expenses/expense-filters'; import { getExpenseColumns, type ExpenseRow } from '@/components/expenses/expense-columns'; import { useCreateFromUrl } from '@/hooks/use-create-from-url'; import { usePaginatedQuery } from '@/hooks/use-paginated-query'; import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation'; import { apiFetch } from '@/lib/api/client'; export default function ExpensesPage() { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const queryClient = useQueryClient(); // Per-port category override. Falls back to shipped defaults until the // vocab call resolves, so the filter bar always renders something. const { data: vocab } = useQuery<{ data: Record }>({ queryKey: ['vocabularies'], queryFn: () => apiFetch('/api/v1/vocabularies'), staleTime: 5 * 60_000, }); const filterDefs = useMemo( () => buildExpenseFilterDefinitions(vocab?.data?.expense_categories), [vocab], ); const [createOpen, setCreateOpen] = useState(false); useCreateFromUrl(() => setCreateOpen(true)); const [editExpense, setEditExpense] = useState(null); const [archiveExpense, setArchiveExpense] = useState(null); const { data, pagination, isLoading, isFetching, sort, setSort, setPage, setPageSize, filters, setFilter, clearFilters, } = usePaginatedQuery({ queryKey: ['expenses'], endpoint: '/api/v1/expenses', filterDefinitions: filterDefs, }); useRealtimeInvalidation({ 'expense:created': [['expenses']], 'expense:updated': [['expenses']], 'expense:archived': [['expenses']], }); const archiveMutation = useMutation({ mutationFn: (id: string) => apiFetch(`/api/v1/expenses/${id}`, { method: 'DELETE' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['expenses'] }); setArchiveExpense(null); }, }); async function handleExport(type: 'csv' | 'pdf') { const res = await fetch(`/api/v1/expenses/export/${type}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(filters), credentials: 'include', }); if (!res.ok) return; const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `expenses.${type}`; a.click(); URL.revokeObjectURL(url); } const columns = getExpenseColumns({ portSlug, onEdit: (expense) => setEditExpense(expense), onArchive: (expense) => setArchiveExpense(expense), }); return (
handleExport('csv')}> Export CSV handleExport('pdf')}> Export PDF
} /> {isLoading ? ( ) : ( { setPage(p); setPageSize(ps); }} sort={sort} onSortChange={setSort} isLoading={isFetching && !isLoading} getRowId={(row) => row.id} cardRender={(row) => ( )} emptyState={ setCreateOpen(true) }} /> } /> )} {editExpense && ( !open && setEditExpense(null)} expense={editExpense} /> )} !open && setArchiveExpense(null)} entityName={archiveExpense?.establishmentName ?? 'this expense'} entityType="Expense" isArchived={false} onConfirm={() => archiveExpense && archiveMutation.mutate(archiveExpense.id)} isLoading={archiveMutation.isPending} /> ); }