/** * Server-side data resolver for the dashboard PDF report. * * Each section is gated on its widget id being present in * `config.widgetIds`, so a report that only includes the pipeline * funnel runs ONE query instead of the full dashboard panel. Keeps * cold-call latency low even when the actual port has hundreds of * berths. * * Lives in its own file (not inside dashboard.service.ts) so the * report-builder concerns — what widget ids map to what fetcher, * which fields the PDF shape requires — stay scoped to the * report-side surface, not the dashboard UI. */ import { getKpis, getPipelineCounts, getBerthStatusDistribution, getHotDeals, getSourceConversion, } from './dashboard.service'; import type { DashboardReportData } from '@/lib/pdf/reports/dashboard-report'; /** * Maps widget ids the dashboard PDF understands. The id space is * intentionally a subset of the on-screen `DASHBOARD_WIDGETS` * registry — only widgets that have a sensible printable form * appear here. The dialog's widget picker filters its option list * by this set. */ export const PDF_DASHBOARD_WIDGET_IDS = [ 'kpi_overview', 'pipeline_funnel', 'berth_status', 'source_conversion', 'hot_deals', ] as const; export type PdfDashboardWidgetId = (typeof PDF_DASHBOARD_WIDGET_IDS)[number]; export interface PdfDashboardWidgetOption { id: PdfDashboardWidgetId; label: string; description: string; } /** * Public widget list (label + description) for the export dialog. * Mirrored from the on-screen widget-registry but with PDF-friendly * copy: a "Berth heat" chart is "Berth demand ranking" in print. */ export const PDF_DASHBOARD_WIDGETS: readonly PdfDashboardWidgetOption[] = [ { id: 'kpi_overview', label: 'Key metrics', description: 'Total clients, active interests, pipeline value, occupancy %.', }, { id: 'pipeline_funnel', label: 'Pipeline funnel', description: 'Active interests grouped by pipeline stage.', }, { id: 'berth_status', label: 'Berth status distribution', description: 'Available / under offer / reserved / sold counts.', }, { id: 'source_conversion', label: 'Source conversion', description: 'Inquiries → Clients → Interests → Won, by lead source.', }, { id: 'hot_deals', label: 'Hot deals', description: 'Top 5 active interests by deal-health score.', }, ]; export async function resolveDashboardReportData( portId: string, widgetIds: string[], ): Promise { const want = new Set(widgetIds); // Each fetcher returns its own shape; default to undefined to // signal "don't render this section" downstream. const data: DashboardReportData = {}; if (want.has('kpi_overview')) { data.kpis = await getKpis(portId); } if (want.has('pipeline_funnel')) { data.pipelineCounts = await getPipelineCounts(portId); } if (want.has('berth_status')) { const dist = await getBerthStatusDistribution(portId); // `dist` shape from the service is already the totals dict; pass // straight through. If the service changes shape, the type-check // here will trip. data.berthStatus = dist; } if (want.has('source_conversion')) { data.sourceConversion = await getSourceConversion(portId); } if (want.has('hot_deals')) { const deals = await getHotDeals(portId, 5); data.hotDeals = deals.map((d) => ({ id: d.id, clientName: d.clientName, mooringNumber: d.mooringNumber, stage: d.stage, lastContact: d.lastContact, })); } return data; }