feat(reports): client / berth / interest list-export PDF reports (phase B)
Extends the report exporter with three list-style report kinds —
clients, berths, interests. Each shares the BrandedReportDocument
layout + the new ReportTable primitive (zebra-striped rows,
proportional widths, no-break rows to keep records together across
page boundaries).
Data fetchers in `src/lib/services/list-report-data.service.ts`:
- resolveClientReportData: clients table joined to per-client
primary email + phone via DISTINCT-style subqueries (matches the
canonical listClients ordering: is_primary DESC, created_at DESC
per channel).
- resolveBerthReportData: berths table, default sort by mooring
number for printed familiarity.
- resolveInterestReportData: interests left-joined to clients +
primary berth, sort by updatedAt desc.
All three cap at 1 000 rows per export with a clear "Showing top N
of <total>" notice rendered when the cap is hit. Above that, the PDF
becomes unreadable (hundreds of pages); reps wanting larger exports
use CSV.
Route schema widened to a 4-arm discriminated union; the dispatch
switch in render-report.ts uses `satisfies` for compile-time variant
narrowing and a `_exhaustive: never` check at the bottom.
UI: each list page (BerthList, ClientList, InterestList) gains an
ExportListPdfButton next to the existing ColumnPicker. Permission-
gated client-side on reports.export; server route re-enforces.
Tests: 3 new render fixtures (1 per kind), all hit the same
%PDF-magic + byte-length assertions. Total render tests now 6/6;
full vitest sweep 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { FilterBar } from '@/components/shared/filter-bar';
|
||||
import { PageHeader } from '@/components/shared/page-header';
|
||||
import { SavedViewsDropdown } from '@/components/shared/saved-views-dropdown';
|
||||
import { ColumnPicker } from '@/components/shared/column-picker';
|
||||
import { ExportListPdfButton } from '@/components/reports/export-list-pdf-button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { EmptyState } from '@/components/shared/empty-state';
|
||||
@@ -136,6 +137,7 @@ export function BerthList() {
|
||||
)}
|
||||
</Button>
|
||||
<ColumnPicker columns={BERTH_COLUMN_OPTIONS} hidden={hidden} onChange={setHidden} />
|
||||
<ExportListPdfButton kind="berths" />
|
||||
{canBulkAdd && (
|
||||
<Button asChild size="sm" variant="default">
|
||||
<Link href={`/${params.portSlug}/admin/berths/bulk-add`}>
|
||||
|
||||
Reference in New Issue
Block a user