Commit Graph

2 Commits

Author SHA1 Message Date
1cdc2fdc6d feat(reports): saved-template store + CRUD + dialog integration (phase C)
Saves rep-configured export setups so a "Monthly board report" or
"Weekly pipeline review" template only has to be assembled once.

Schema (migration 0079_report_templates.sql + drizzle entry):
  - report_templates: id, port_id, kind, name, description, config
    (jsonb), created_by, created_at, updated_at.
  - Sibling-name uniqueness scoped (port_id, kind, LOWER(name)) so
    Port A and Port B can both have "Quarterly review" without
    colliding, and two different KINDS in the same port can share a
    name (a clients "Quarterly review" + an interests "Quarterly
    review" coexist).
  - port_id FK cascades on delete; templates evaporate with the
    parent port. No cross-port enumeration risk since every query
    filters by port_id.

Service (src/lib/services/report-templates.service.ts):
  - createReportTemplate / listReportTemplates / getReportTemplate /
    updateReportTemplate / deleteReportTemplate.
  - Audit-logs every write with old/new values for the rename case.
  - Surfaces sibling-name collisions as ConflictError with a
    rep-readable message ('A "Monthly board report" template
    already exists for the dashboard kind').

Routes:
  - GET  /api/v1/reports/templates?kind=clients
  - POST /api/v1/reports/templates
  - GET  /api/v1/reports/templates/[id]
  - PATCH /api/v1/reports/templates/[id]
  - DELETE /api/v1/reports/templates/[id]
  All gated on `reports.export` — same permission as generating
  reports lets the rep manage the templates that drive them.
  POST cross-validates that `body.kind === body.config.kind` so a
  rep can't sneak a dashboard config into a clients template and
  confuse the rendering path at use time.

UI:
  - SavedTemplatesPicker reusable component — dropdown of templates
    for this port + kind, inline "Save as template" toggle that
    expands to a name input + Save button, delete button next to
    the picker once a template is selected.
  - Wired into both ExportDashboardPdfButton + ExportListPdfButton.
    Applying a saved template hydrates the dialog's form (selected
    widgets / filters / title) from the saved config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:46:52 +02:00
3b199c245c feat(reports): PDF report exporter foundation + dashboard report (phase A)
Production-grade PDF reporting for the CRM. Phase A ships the
foundation (branded layout, render pipeline, API route) plus the
first report kind — the dashboard summary. Phases B, C, D add the
remaining report kinds, saved templates, and the preview modal.

Stack: @react-pdf/renderer (already in package.json). Single primary
font (Helvetica/Helvetica-Bold), per-port primary color + logo,
table-based section layout. Charts will become tables here on
purpose; reports are for printed reference and review, where
exact numbers beat at-a-glance shapes. We can revisit Recharts-as-
SVG embedding if a stakeholder asks for chart visuals.

New files:
  - src/lib/pdf/reports/types.ts: discriminated-union ReportConfig
    covering dashboard / clients / berths / interests kinds. Only
    dashboard is wired in phase A; the others throw a clear
    not-implemented error from pickDocument().
  - src/lib/pdf/reports/styles.ts: shared StyleSheet keyed off
    branding.primaryColor. Computes a readable foreground color
    (luminance check) for the accent stripe so dark-brand ports
    still read at AA.
  - src/lib/pdf/reports/branded-document.tsx: page wrapper with
    fixed footer (port name, generated-at timestamp, page numbers
    via react-pdf's render-prop pattern).
  - src/lib/pdf/reports/dashboard-report.tsx: KPI grid + per-widget
    SimpleTable sections. Each section gated on the widget id being
    present in config.widgetIds AND data being supplied.
  - src/lib/pdf/reports/render-report.ts: single entry point that
    resolves branding (logoUrl + primaryColor + portName from
    getPortBrandingConfig + ports.name), dispatches via
    discriminated-union switch, returns Buffer via renderToBuffer.
    Exhaustiveness check at the bottom catches unhandled variants
    at compile time.
  - src/lib/services/dashboard-report-data.service.ts: server-side
    data resolver. PDF_DASHBOARD_WIDGETS is the public widget list
    for the dialog picker; each id maps to a dashboard.service.ts
    fetcher invoked only when the rep selected that widget.
  - src/app/api/v1/reports/generate/route.ts: POST endpoint, zod
    discriminated-union body schema, withAuth + withPermission
    'reports.export' gating, audit-log write on success, RFC 5987
    Content-Disposition for unicode-safe filenames.
  - src/components/reports/export-dashboard-pdf-button.tsx: dialog
    with section checkboxes + title input. Permission-gated client-
    side (server re-checks). Raw fetch (not apiFetch) to pull the
    binary blob with X-Port-Id header attached manually.
  - tests/unit/pdf-report-renderer.test.ts: renders three fixture
    cases — full set / sparse / no-logo — and asserts the buffer
    starts with the `%PDF-` magic bytes and is non-trivial in size.

DashboardShell gains an Export PDF button between the date-range
picker and the Customize widgets menu (gated on reports.export).

Verified: tsc clean, vitest 1451/1451 (3 new render tests included).
The first end-to-end manual test (export a real dashboard) is in
Phase D after the preview modal lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:35:53 +02:00