diff --git a/src/app/api/v1/reports/generate/route.ts b/src/app/api/v1/reports/generate/route.ts index e9ff2dd5..f2e67b96 100644 --- a/src/app/api/v1/reports/generate/route.ts +++ b/src/app/api/v1/reports/generate/route.ts @@ -7,6 +7,11 @@ import { errorResponse, ValidationError } from '@/lib/errors'; import { logger } from '@/lib/logger'; import { renderReport, type ReportData } from '@/lib/pdf/reports/render-report'; import { resolveDashboardReportData } from '@/lib/services/dashboard-report-data.service'; +import { + resolveClientReportData, + resolveBerthReportData, + resolveInterestReportData, +} from '@/lib/services/list-report-data.service'; import { createAuditLog } from '@/lib/audit'; const dashboardConfigSchema = z.object({ @@ -22,12 +27,34 @@ const dashboardConfigSchema = z.object({ .optional(), }); +const listFilters = z.object({ includeArchived: z.boolean().optional() }).passthrough().optional(); + +const clientsConfigSchema = z.object({ + kind: z.literal('clients'), + columns: z.array(z.string()).max(20).optional(), + filters: listFilters, +}); + +const berthsConfigSchema = z.object({ + kind: z.literal('berths'), + columns: z.array(z.string()).max(20).optional(), + filters: listFilters, +}); + +const interestsConfigSchema = z.object({ + kind: z.literal('interests'), + columns: z.array(z.string()).max(20).optional(), + filters: listFilters, +}); + const requestSchema = z.object({ title: z.string().min(1).max(200), subtitle: z.string().max(400).optional(), config: z.discriminatedUnion('kind', [ dashboardConfigSchema, - // Phase B will widen this union with clients / berths / interests. + clientsConfigSchema, + berthsConfigSchema, + interestsConfigSchema, ]), }); @@ -59,10 +86,33 @@ export const POST = withAuth( case 'dashboard': data.dashboard = await resolveDashboardReportData(ctx.portId, body.config.widgetIds); break; - default: - // Unreachable while only the dashboard kind is wired; kept - // for the type-narrowing exhaustiveness check. - throw new ValidationError('Unsupported report kind'); + case 'clients': + data.clients = await resolveClientReportData(ctx.portId, { + includeArchived: Boolean( + (body.config.filters as { includeArchived?: boolean } | undefined)?.includeArchived, + ), + }); + break; + case 'berths': + data.berths = await resolveBerthReportData(ctx.portId, { + includeArchived: Boolean( + (body.config.filters as { includeArchived?: boolean } | undefined)?.includeArchived, + ), + }); + break; + case 'interests': + data.interests = await resolveInterestReportData(ctx.portId, { + includeArchived: Boolean( + (body.config.filters as { includeArchived?: boolean } | undefined)?.includeArchived, + ), + }); + break; + default: { + const _exhaustive: never = body.config; + throw new ValidationError( + `Unsupported report kind: ${(_exhaustive as { kind: string }).kind}`, + ); + } } const buffer = await renderReport({ diff --git a/src/components/berths/berth-list.tsx b/src/components/berths/berth-list.tsx index fea78537..c465ef48 100644 --- a/src/components/berths/berth-list.tsx +++ b/src/components/berths/berth-list.tsx @@ -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() { )}