Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
'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';
|
feat(mobile): mobile cards for yachts, companies, berths, invoices, expenses
Five new <EntityCard> files using the shared <ListCard> shell, wired
into each list page's <DataTable> via cardRender. Desktop view
(lg+) is unchanged.
- YachtCard: Ship icon, owner subtitle (User/Building2 icon by
ownerType), dimensions in meters preferred, hull #,
status pill. No accent bar (status is free-text).
- CompanyCard: Building2 icon, legalName subtitle, country (MapPin)
+ tax id (Hash) meta, member/yacht count line.
- BerthCard: Anchor icon, area subtitle (MapPin), dimensions
meta, status pill. Status-encoded accent bar
(emerald=available, amber=under_offer, slate=sold).
- InvoiceCard: FileText icon, client subtitle, due date (Calendar)
meta, prominent currency-formatted amount. Status
accent bar (emerald=paid, orange=overdue, ...).
- ExpenseCard: Receipt icon, category subtitle, expense date meta,
prominent amount, payment-status pill, "Possible
duplicate" pill when duplicateOf is set. Accent bar
by paymentStatus, overridden to amber when flagged
as duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:34:04 +02:00
|
|
|
import { InvoiceCard } from '@/components/invoices/invoice-card';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
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({
|
feat(mobile): mobile cards for yachts, companies, berths, invoices, expenses
Five new <EntityCard> files using the shared <ListCard> shell, wired
into each list page's <DataTable> via cardRender. Desktop view
(lg+) is unchanged.
- YachtCard: Ship icon, owner subtitle (User/Building2 icon by
ownerType), dimensions in meters preferred, hull #,
status pill. No accent bar (status is free-text).
- CompanyCard: Building2 icon, legalName subtitle, country (MapPin)
+ tax id (Hash) meta, member/yacht count line.
- BerthCard: Anchor icon, area subtitle (MapPin), dimensions
meta, status pill. Status-encoded accent bar
(emerald=available, amber=under_offer, slate=sold).
- InvoiceCard: FileText icon, client subtitle, due date (Calendar)
meta, prominent currency-formatted amount. Status
accent bar (emerald=paid, orange=overdue, ...).
- ExpenseCard: Receipt icon, category subtitle, expense date meta,
prominent amount, payment-status pill, "Possible
duplicate" pill when duplicateOf is set. Accent bar
by paymentStatus, overridden to amber when flagged
as duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:34:04 +02:00
|
|
|
mutationFn: (id: string) => apiFetch(`/api/v1/invoices/${id}`, { method: 'DELETE' }),
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
|
|
|
|
setDeleteTarget(null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const sendMutation = useMutation({
|
feat(mobile): mobile cards for yachts, companies, berths, invoices, expenses
Five new <EntityCard> files using the shared <ListCard> shell, wired
into each list page's <DataTable> via cardRender. Desktop view
(lg+) is unchanged.
- YachtCard: Ship icon, owner subtitle (User/Building2 icon by
ownerType), dimensions in meters preferred, hull #,
status pill. No accent bar (status is free-text).
- CompanyCard: Building2 icon, legalName subtitle, country (MapPin)
+ tax id (Hash) meta, member/yacht count line.
- BerthCard: Anchor icon, area subtitle (MapPin), dimensions
meta, status pill. Status-encoded accent bar
(emerald=available, amber=under_offer, slate=sold).
- InvoiceCard: FileText icon, client subtitle, due date (Calendar)
meta, prominent currency-formatted amount. Status
accent bar (emerald=paid, orange=overdue, ...).
- ExpenseCard: Receipt icon, category subtitle, expense date meta,
prominent amount, payment-status pill, "Possible
duplicate" pill when duplicateOf is set. Accent bar
by paymentStatus, overridden to amber when flagged
as duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:34:04 +02:00
|
|
|
mutationFn: (id: string) => apiFetch(`/api/v1/invoices/${id}/send`, { method: 'POST' }),
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const columns = getInvoiceColumns({
|
|
|
|
|
portSlug,
|
|
|
|
|
onSend: (invoice) => sendMutation.mutate(invoice.id),
|
feat(mobile): mobile cards for yachts, companies, berths, invoices, expenses
Five new <EntityCard> files using the shared <ListCard> shell, wired
into each list page's <DataTable> via cardRender. Desktop view
(lg+) is unchanged.
- YachtCard: Ship icon, owner subtitle (User/Building2 icon by
ownerType), dimensions in meters preferred, hull #,
status pill. No accent bar (status is free-text).
- CompanyCard: Building2 icon, legalName subtitle, country (MapPin)
+ tax id (Hash) meta, member/yacht count line.
- BerthCard: Anchor icon, area subtitle (MapPin), dimensions
meta, status pill. Status-encoded accent bar
(emerald=available, amber=under_offer, slate=sold).
- InvoiceCard: FileText icon, client subtitle, due date (Calendar)
meta, prominent currency-formatted amount. Status
accent bar (emerald=paid, orange=overdue, ...).
- ExpenseCard: Receipt icon, category subtitle, expense date meta,
prominent amount, payment-status pill, "Possible
duplicate" pill when duplicateOf is set. Accent bar
by paymentStatus, overridden to amber when flagged
as duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:34:04 +02:00
|
|
|
onRecordPayment: (invoice) => router.push(`/${portSlug}/invoices/${invoice.id}?tab=payment`),
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
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}
|
feat(mobile): mobile cards for yachts, companies, berths, invoices, expenses
Five new <EntityCard> files using the shared <ListCard> shell, wired
into each list page's <DataTable> via cardRender. Desktop view
(lg+) is unchanged.
- YachtCard: Ship icon, owner subtitle (User/Building2 icon by
ownerType), dimensions in meters preferred, hull #,
status pill. No accent bar (status is free-text).
- CompanyCard: Building2 icon, legalName subtitle, country (MapPin)
+ tax id (Hash) meta, member/yacht count line.
- BerthCard: Anchor icon, area subtitle (MapPin), dimensions
meta, status pill. Status-encoded accent bar
(emerald=available, amber=under_offer, slate=sold).
- InvoiceCard: FileText icon, client subtitle, due date (Calendar)
meta, prominent currency-formatted amount. Status
accent bar (emerald=paid, orange=overdue, ...).
- ExpenseCard: Receipt icon, category subtitle, expense date meta,
prominent amount, payment-status pill, "Possible
duplicate" pill when duplicateOf is set. Accent bar
by paymentStatus, overridden to amber when flagged
as duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:34:04 +02:00
|
|
|
cardRender={(row) => (
|
|
|
|
|
<InvoiceCard
|
|
|
|
|
invoice={row.original}
|
|
|
|
|
portSlug={portSlug}
|
|
|
|
|
onSend={(invoice) => sendMutation.mutate(invoice.id)}
|
|
|
|
|
onRecordPayment={(invoice) =>
|
|
|
|
|
router.push(`/${portSlug}/invoices/${invoice.id}?tab=payment`)
|
|
|
|
|
}
|
|
|
|
|
onDelete={setDeleteTarget}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
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{' '}
|
feat(mobile): mobile cards for yachts, companies, berths, invoices, expenses
Five new <EntityCard> files using the shared <ListCard> shell, wired
into each list page's <DataTable> via cardRender. Desktop view
(lg+) is unchanged.
- YachtCard: Ship icon, owner subtitle (User/Building2 icon by
ownerType), dimensions in meters preferred, hull #,
status pill. No accent bar (status is free-text).
- CompanyCard: Building2 icon, legalName subtitle, country (MapPin)
+ tax id (Hash) meta, member/yacht count line.
- BerthCard: Anchor icon, area subtitle (MapPin), dimensions
meta, status pill. Status-encoded accent bar
(emerald=available, amber=under_offer, slate=sold).
- InvoiceCard: FileText icon, client subtitle, due date (Calendar)
meta, prominent currency-formatted amount. Status
accent bar (emerald=paid, orange=overdue, ...).
- ExpenseCard: Receipt icon, category subtitle, expense date meta,
prominent amount, payment-status pill, "Possible
duplicate" pill when duplicateOf is set. Accent bar
by paymentStatus, overridden to amber when flagged
as duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:34:04 +02:00
|
|
|
<span className="font-mono font-medium">{deleteTarget.invoiceNumber}</span>. This
|
|
|
|
|
action cannot be undone.
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</p>
|
|
|
|
|
<div className="flex items-center gap-2 justify-end">
|
feat(mobile): mobile cards for yachts, companies, berths, invoices, expenses
Five new <EntityCard> files using the shared <ListCard> shell, wired
into each list page's <DataTable> via cardRender. Desktop view
(lg+) is unchanged.
- YachtCard: Ship icon, owner subtitle (User/Building2 icon by
ownerType), dimensions in meters preferred, hull #,
status pill. No accent bar (status is free-text).
- CompanyCard: Building2 icon, legalName subtitle, country (MapPin)
+ tax id (Hash) meta, member/yacht count line.
- BerthCard: Anchor icon, area subtitle (MapPin), dimensions
meta, status pill. Status-encoded accent bar
(emerald=available, amber=under_offer, slate=sold).
- InvoiceCard: FileText icon, client subtitle, due date (Calendar)
meta, prominent currency-formatted amount. Status
accent bar (emerald=paid, orange=overdue, ...).
- ExpenseCard: Receipt icon, category subtitle, expense date meta,
prominent amount, payment-status pill, "Possible
duplicate" pill when duplicateOf is set. Accent bar
by paymentStatus, overridden to amber when flagged
as duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:34:04 +02:00
|
|
|
<Button variant="outline" size="sm" onClick={() => setDeleteTarget(null)}>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|