117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<DashboardReportData> {
|
||
|
|
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;
|
||
|
|
}
|