feat(reports-overhaul): sales + operational + custom reports, templates, schedules, exports

End-to-end reports build covering Phases 1, 2, 5, 6, 7 of Initiative 1
in docs/launch-readiness.md. Phases 3 (Marketing) + 4 (Financial)
remain deferred per the gap audit at the bottom of that doc.

Highlights:
- Sales performance report: 7 KPI tiles, pipeline funnel + stage
  velocity + win-rate-over-time + source conversion + rep leaderboard
  charts, deal-heat section, 5 detail tables, stage / lead-cat /
  outcome filters.
- Operational report: 7 KPIs, 7 charts (heatmap, status mix, tenancy
  churn, tenure histogram, signing box plot, occupancy by area, docs
  in pipeline), 4 tables. Module-OFF banner when tenancies disabled.
- Custom (ad-hoc) builder v1: 4 entities (clients, interests, berths,
  tenancies), column-whitelist composer, date filter, CSV download,
  save-as-template. Registry-only extension path for the remaining 6
  entities documented at src/lib/reports/custom/registry.ts.
- Templates: load / modify / save / save-as on Sales / Operational /
  Custom. ?templateId= URL deep-link hydration via useRef guard.
  Active-template badge clears when the user drives view-state via
  wrapped setters; raw setters used on template apply so the badge
  survives.
- Scheduled runs: BullMQ poll fires due schedules, mints report_runs,
  renders, optionally emails. Recipients optional (zero-recipient
  schedules archive without sending). PDF-only output for v1.
  Schedule dialog re-mounts via key prop on schedule.id transitions
  to avoid setState-in-effect reset patterns.
- Server-side PDF endpoint + shared payload renderer
  (lib/pdf/reports/payload-report.tsx) so client + scheduler share
  one rendering path.
- Shared currency formatter (lib/reports/format-currency.ts)
  consolidates 5 duplicated formatMoney helpers; fixes hardcoded
  'USD' in detail tables; pre-formats money rows so PDF export
  (which strips column.format callbacks at the JSON boundary)
  renders consistently with CSV / XLSX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 22:41:53 +02:00
parent 909dd44605
commit 3bdf59e917
41 changed files with 10704 additions and 203 deletions

View File

@@ -28,7 +28,15 @@ export type ListReportsInput = z.infer<typeof listReportsSchema>;
// adds the CRUD layer on the new tables. The legacy `generatedReports` flow
// above stays for the existing dashboard-export button until it migrates.
export const REPORT_KINDS = ['dashboard', 'clients', 'berths', 'interests'] as const;
export const REPORT_KINDS = [
'dashboard',
'clients',
'berths',
'interests',
'sales',
'operational',
'custom',
] as const;
export type ReportKind = (typeof REPORT_KINDS)[number];
export const REPORT_OUTPUT_FORMATS = ['pdf', 'csv', 'png'] as const;
@@ -87,7 +95,11 @@ export type ListReportSchedulesInput = z.infer<typeof listReportSchedulesSchema>
export const createReportScheduleSchema = z.object({
templateId: z.string().min(1),
cadence: z.enum(REPORT_SCHEDULE_CADENCES),
recipients: z.array(recipientSchema).min(1).max(50),
// Empty recipients list = "run + archive, don't email". Per locked
// decision (2026-05-27): auto-email is OPTIONAL — an admin can
// schedule a run that just appears in /reports/runs without
// forcing an email blast.
recipients: z.array(recipientSchema).max(50).default([]),
outputFormat: z.enum(REPORT_OUTPUT_FORMATS).default('pdf'),
enabled: z.boolean().default(true),
});
@@ -95,7 +107,7 @@ export type CreateReportScheduleInput = z.infer<typeof createReportScheduleSchem
export const updateReportScheduleSchema = z.object({
cadence: z.enum(REPORT_SCHEDULE_CADENCES).optional(),
recipients: z.array(recipientSchema).min(1).max(50).optional(),
recipients: z.array(recipientSchema).max(50).optional(),
outputFormat: z.enum(REPORT_OUTPUT_FORMATS).optional(),
enabled: z.boolean().optional(),
});