Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
299 lines
11 KiB
TypeScript
299 lines
11 KiB
TypeScript
/**
|
|
* Pure data + types for the dashboard-PDF report's widget catalogue.
|
|
*
|
|
* Lives in its own file so client-side surfaces (the "Export as PDF"
|
|
* dialog) can import the catalogue without dragging the server-side
|
|
* resolver - which imports the DB layer - into the client bundle.
|
|
* Before this split, `export-dashboard-pdf-button.tsx` pulled
|
|
* `PDF_DASHBOARD_WIDGETS` from `dashboard-report-data.service.ts`,
|
|
* whose top-level `import { getKpis } from './dashboard.service'`
|
|
* dragged in `postgres` and crashed the client build with
|
|
* "Module not found: Can't resolve 'fs'".
|
|
*/
|
|
|
|
/**
|
|
* Every selectable widget on the export dialog. Each entry maps 1:1
|
|
* to a section in `dashboard-report.tsx` and a fetcher in
|
|
* `resolveDashboardReportData()`. Categorised so the dialog can
|
|
* render grouped checkboxes (Charts / Tables / Time-period cohorts /
|
|
* Activity & people).
|
|
*
|
|
* Convention: where a widget has both a chart-style and a
|
|
* table-style variant of the same underlying data (pipeline funnel
|
|
* is the canonical case), we expose BOTH ids and let the rep pick.
|
|
* `*_chart` ids render an SVG chart inline; `*_table` ids render
|
|
* the same data as a SimpleTable.
|
|
*
|
|
* Backwards-compat: the legacy ids `pipeline_funnel`, `berth_status`
|
|
* and `source_conversion` remain valid (they render as tables -
|
|
* the original behaviour) so existing saved-templates keep working.
|
|
*/
|
|
export const PDF_DASHBOARD_WIDGET_IDS = [
|
|
// ─── KPI / summary
|
|
'kpi_overview',
|
|
|
|
// ─── Pipeline ────────────────────────────────────────────────────
|
|
'pipeline_funnel', // legacy: table render
|
|
'pipeline_funnel_chart', // bar-chart variant
|
|
'pipeline_value_breakdown', // $ per stage
|
|
'stage_conversion_rates', // % step→step conversion
|
|
'avg_sales_cycle', // days from new→won
|
|
'revenue_forecast', // weighted forecast snapshot
|
|
|
|
// ─── Berths ──────────────────────────────────────────────────────
|
|
'berth_status', // legacy: table render
|
|
'berth_status_donut', // donut chart variant
|
|
'berth_demand_ranking', // top berths by interest count
|
|
'occupancy_timeline_chart', // line over date range
|
|
|
|
// ─── Source / leads
|
|
'source_conversion', // legacy: table render
|
|
'source_conversion_chart', // bar-chart variant
|
|
'lead_source_donut', // donut of source mix
|
|
'inquiry_inbox_summary', // inbound inquiry counts
|
|
|
|
// ─── Time-period cohorts (require date-range filter) ─────────────
|
|
'new_clients_period',
|
|
'new_interests_period',
|
|
'signed_documents_period',
|
|
'deposits_received_period',
|
|
'contracts_signed_period',
|
|
'berths_sold_period',
|
|
|
|
// ─── Deals & activity ────────────────────────────────────────────
|
|
'hot_deals',
|
|
'deal_pulse_distribution',
|
|
'recent_activity',
|
|
'reminders_summary',
|
|
|
|
// ─── Geography / clients ─────────────────────────────────────────
|
|
'client_country_distribution',
|
|
] as const;
|
|
|
|
export type PdfDashboardWidgetId = (typeof PDF_DASHBOARD_WIDGET_IDS)[number];
|
|
|
|
export type PdfDashboardWidgetCategory =
|
|
| 'summary'
|
|
| 'pipeline'
|
|
| 'berths'
|
|
| 'sources'
|
|
| 'period'
|
|
| 'deals'
|
|
| 'geography';
|
|
|
|
export interface PdfDashboardWidgetOption {
|
|
id: PdfDashboardWidgetId;
|
|
label: string;
|
|
description: string;
|
|
category: PdfDashboardWidgetCategory;
|
|
/** When true, this widget requires a date-range filter on the
|
|
* export dialog. The form should grey the option out until the
|
|
* rep picks a window. */
|
|
requiresPeriod?: boolean;
|
|
/** True when the widget renders as a chart (SVG) not a table.
|
|
* Used by the export dialog to show a small icon distinguishing
|
|
* chart variants from their table siblings. */
|
|
isChart?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Public widget list for the export dialog. New widgets land here;
|
|
* the resolver picks them up automatically.
|
|
*/
|
|
export const PDF_DASHBOARD_WIDGETS: readonly PdfDashboardWidgetOption[] = [
|
|
// ─── Summary ─────────────────────────────────────────────────────
|
|
{
|
|
id: 'kpi_overview',
|
|
label: 'Key metrics',
|
|
description: 'Total clients, active interests, pipeline value, occupancy %.',
|
|
category: 'summary',
|
|
},
|
|
|
|
// ─── Pipeline ────────────────────────────────────────────────────
|
|
{
|
|
id: 'pipeline_funnel_chart',
|
|
label: 'Pipeline funnel · chart',
|
|
description: 'Active interests per pipeline stage rendered as a horizontal bar chart.',
|
|
category: 'pipeline',
|
|
isChart: true,
|
|
},
|
|
{
|
|
id: 'pipeline_funnel',
|
|
label: 'Pipeline funnel · table',
|
|
description: 'Same data as the chart, rendered as a numbers-first table.',
|
|
category: 'pipeline',
|
|
},
|
|
{
|
|
id: 'pipeline_value_breakdown',
|
|
label: 'Pipeline value breakdown',
|
|
description: 'Pipeline value per stage with weighted forecast in port currency.',
|
|
category: 'pipeline',
|
|
},
|
|
{
|
|
id: 'stage_conversion_rates',
|
|
label: 'Stage conversion rates',
|
|
description: '% of interests that advance from each pipeline stage to the next.',
|
|
category: 'pipeline',
|
|
},
|
|
{
|
|
id: 'avg_sales_cycle',
|
|
label: 'Average sales cycle',
|
|
description: 'Median + mean days from "new enquiry" to "contract signed".',
|
|
category: 'pipeline',
|
|
},
|
|
{
|
|
id: 'revenue_forecast',
|
|
label: 'Revenue forecast snapshot',
|
|
description: 'Weighted pipeline value over the next 30 / 60 / 90 days.',
|
|
category: 'pipeline',
|
|
},
|
|
|
|
// ─── Berths ──────────────────────────────────────────────────────
|
|
{
|
|
id: 'berth_status_donut',
|
|
label: 'Berth status · donut',
|
|
description: 'Available / under offer / sold / maintenance as a donut chart.',
|
|
category: 'berths',
|
|
isChart: true,
|
|
},
|
|
{
|
|
id: 'berth_status',
|
|
label: 'Berth status · table',
|
|
description: 'Same data as the donut, rendered as counts + percentages.',
|
|
category: 'berths',
|
|
},
|
|
{
|
|
id: 'berth_demand_ranking',
|
|
label: 'Berth demand ranking',
|
|
description: 'Top 10 berths by active-interest count + heat tier (A/B/C/D).',
|
|
category: 'berths',
|
|
},
|
|
{
|
|
id: 'occupancy_timeline_chart',
|
|
label: 'Occupancy timeline · chart',
|
|
description: 'Daily berth occupancy rate over the report period, line chart.',
|
|
category: 'berths',
|
|
isChart: true,
|
|
requiresPeriod: true,
|
|
},
|
|
|
|
// ─── Sources ─────────────────────────────────────────────────────
|
|
{
|
|
id: 'source_conversion_chart',
|
|
label: 'Source conversion · chart',
|
|
description: 'Win rate per lead source as a horizontal bar chart.',
|
|
category: 'sources',
|
|
isChart: true,
|
|
},
|
|
{
|
|
id: 'source_conversion',
|
|
label: 'Source conversion · table',
|
|
description: 'Same data as the chart, with raw won/lost counts.',
|
|
category: 'sources',
|
|
},
|
|
{
|
|
id: 'lead_source_donut',
|
|
label: 'Lead source mix · donut',
|
|
description: 'Share of new interests by lead source, donut chart.',
|
|
category: 'sources',
|
|
isChart: true,
|
|
},
|
|
{
|
|
id: 'inquiry_inbox_summary',
|
|
label: 'Inbound inquiry summary',
|
|
description: 'Counts + triage outcomes for public-site inquiries over the period.',
|
|
category: 'sources',
|
|
requiresPeriod: true,
|
|
},
|
|
|
|
// ─── Period cohorts ──────────────────────────────────────────────
|
|
{
|
|
id: 'new_clients_period',
|
|
label: 'New clients (in period)',
|
|
description: 'Clients added during the report window, with source.',
|
|
category: 'period',
|
|
requiresPeriod: true,
|
|
},
|
|
{
|
|
id: 'new_interests_period',
|
|
label: 'New interests (in period)',
|
|
description: 'Interests opened during the window, with stage + value.',
|
|
category: 'period',
|
|
requiresPeriod: true,
|
|
},
|
|
{
|
|
id: 'signed_documents_period',
|
|
label: 'Documents signed (in period)',
|
|
description: 'EOIs / reservations / contracts signed during the window.',
|
|
category: 'period',
|
|
requiresPeriod: true,
|
|
},
|
|
{
|
|
id: 'deposits_received_period',
|
|
label: 'Deposits received (in period)',
|
|
description: 'Deposit payments received during the window, with $ totals.',
|
|
category: 'period',
|
|
requiresPeriod: true,
|
|
},
|
|
{
|
|
id: 'contracts_signed_period',
|
|
label: 'Contracts signed (in period)',
|
|
description: 'Contract documents marked signed during the window.',
|
|
category: 'period',
|
|
requiresPeriod: true,
|
|
},
|
|
{
|
|
id: 'berths_sold_period',
|
|
label: 'Berths sold (in period)',
|
|
description: 'Berths transitioned to Sold status during the window.',
|
|
category: 'period',
|
|
requiresPeriod: true,
|
|
},
|
|
|
|
// ─── Deals & activity ────────────────────────────────────────────
|
|
{
|
|
id: 'hot_deals',
|
|
label: 'Hot deals',
|
|
description: 'Top active interests ranked by stage + recent activity (table).',
|
|
category: 'deals',
|
|
},
|
|
{
|
|
id: 'deal_pulse_distribution',
|
|
label: 'Deal pulse distribution',
|
|
description: 'Counts of interests in each pulse tier (hot / warm / cool / cold).',
|
|
category: 'deals',
|
|
},
|
|
{
|
|
id: 'recent_activity',
|
|
label: 'Recent activity snapshot',
|
|
description: 'Last 20 audit-log entries against this port (compact log).',
|
|
category: 'deals',
|
|
},
|
|
{
|
|
id: 'reminders_summary',
|
|
label: 'Reminders summary',
|
|
description: 'Open + completed reminder counts per rep over the window.',
|
|
category: 'deals',
|
|
requiresPeriod: true,
|
|
},
|
|
|
|
// ─── Geography ───────────────────────────────────────────────────
|
|
{
|
|
id: 'client_country_distribution',
|
|
label: 'Client country distribution',
|
|
description: 'Active-client counts grouped by nationality.',
|
|
category: 'geography',
|
|
},
|
|
];
|
|
|
|
/** Buckets the catalog into UI sections for the export dialog. */
|
|
export const PDF_DASHBOARD_CATEGORY_LABELS: Record<PdfDashboardWidgetCategory, string> = {
|
|
summary: 'Summary',
|
|
pipeline: 'Pipeline',
|
|
berths: 'Berths',
|
|
sources: 'Lead sources',
|
|
period: 'Time-period cohorts',
|
|
deals: 'Deals & activity',
|
|
geography: 'Geography',
|
|
};
|