Files
pn-new-crm/src/app/(dashboard)/[portSlug]/invoices/page.tsx

191 lines
6.0 KiB
TypeScript
Raw Normal View History

'use client';
import { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Plus, Trash2 } from 'lucide-react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
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 { PermissionGate } from '@/components/shared/permission-gate';
import { invoiceFilterDefinitions } from '@/components/invoices/invoice-filters';
import { getInvoiceColumns, type InvoiceRow } from '@/components/invoices/invoice-columns';
import { usePaginatedQuery } from '@/hooks/use-paginated-query';
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
import { apiFetch } from '@/lib/api/client';
const STATUS_TABS = [
{ label: 'All', value: '' },
{ label: 'Draft', value: 'draft' },
{ label: 'Sent', value: 'sent' },
{ label: 'Paid', value: 'paid' },
{ label: 'Overdue', value: 'overdue' },
] as const;
export default function InvoicesPage() {
const params = useParams<{ portSlug: string }>();
const portSlug = params?.portSlug ?? '';
const router = useRouter();
const queryClient = useQueryClient();
const [deleteTarget, setDeleteTarget] = useState<InvoiceRow | null>(null);
const {
data,
pagination,
isLoading,
isFetching,
sort,
setSort,
setPage,
setPageSize,
filters,
setFilter,
clearFilters,
} = usePaginatedQuery<InvoiceRow>({
queryKey: ['invoices'],
endpoint: '/api/v1/invoices',
filterDefinitions: invoiceFilterDefinitions,
});
const activeStatus = (filters.status as string) ?? '';
useRealtimeInvalidation({
'invoice:created': [['invoices']],
'invoice:updated': [['invoices']],
'invoice:sent': [['invoices']],
'invoice:paid': [['invoices']],
'invoice:overdue': [['invoices']],
});
const deleteMutation = useMutation({
mutationFn: (id: string) =>
apiFetch(`/api/v1/invoices/${id}`, { method: 'DELETE' }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['invoices'] });
setDeleteTarget(null);
},
});
const sendMutation = useMutation({
mutationFn: (id: string) =>
apiFetch(`/api/v1/invoices/${id}/send`, { method: 'POST' }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['invoices'] });
},
});
const columns = getInvoiceColumns({
portSlug,
onSend: (invoice) => sendMutation.mutate(invoice.id),
onRecordPayment: (invoice) =>
router.push(`/${portSlug}/invoices/${invoice.id}?tab=payment`),
onDelete: (invoice) => setDeleteTarget(invoice),
});
return (
<div className="space-y-4">
<PageHeader
title="Invoices"
description="Create and manage port invoices"
actions={
<PermissionGate resource="invoices" action="create">
<Button size="sm" onClick={() => router.push(`/${portSlug}/invoices/new`)}>
<Plus className="mr-1.5 h-4 w-4" />
New Invoice
</Button>
</PermissionGate>
}
/>
{/* Status quick-filter tabs */}
<div className="flex items-center gap-1 border-b">
{STATUS_TABS.map((tab) => (
<button
key={tab.value}
onClick={() => setFilter('status', tab.value || undefined)}
className={`px-3 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
activeStatus === tab.value
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'
}`}
>
{tab.label}
</button>
))}
</div>
<FilterBar
filters={invoiceFilterDefinitions.filter((f) => f.key !== 'status')}
values={filters}
onChange={setFilter}
onClear={clearFilters}
/>
{isLoading ? (
<TableSkeleton />
) : (
<DataTable
columns={columns}
data={data}
pagination={pagination}
onPaginationChange={(p, ps) => {
setPage(p);
setPageSize(ps);
}}
sort={sort}
onSortChange={setSort}
isLoading={isFetching && !isLoading}
getRowId={(row) => row.id}
emptyState={
<EmptyState
title="No invoices found"
description="Get started by creating your first invoice."
action={{
label: 'New Invoice',
onClick: () => router.push(`/${portSlug}/invoices/new`),
}}
/>
}
/>
)}
{/* Delete confirmation */}
{deleteTarget && (
<div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center">
<div className="bg-background border rounded-lg shadow-lg p-6 max-w-sm w-full space-y-4">
<h3 className="font-semibold">Delete Invoice?</h3>
<p className="text-sm text-muted-foreground">
This will permanently delete invoice{' '}
<span className="font-mono font-medium">{deleteTarget.invoiceNumber}</span>.
This action cannot be undone.
</p>
<div className="flex items-center gap-2 justify-end">
<Button
variant="outline"
size="sm"
onClick={() => setDeleteTarget(null)}
>
Cancel
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => deleteMutation.mutate(deleteTarget.id)}
disabled={deleteMutation.isPending}
>
<Trash2 className="mr-1.5 h-4 w-4" />
Delete
</Button>
</div>
</div>
</div>
)}
</div>
);
}