Files
pn-new-crm/src/lib/pdf/templates/reports/revenue-report.tsx

100 lines
2.7 KiB
TypeScript
Raw Normal View History

feat(reports): migrate 4 reports from pdfme to react-pdf Phase 1 / commits 3-6 of 14 — bundled because every report follows the same conversion pattern (coordinate-stuffed pdfme template -> JSX brand kit). Each report now has a real header (logo + port name), structured KeyValueGrid for summary stats, a chart (BarChart / FunnelChart / PieChart / LineChart-ready), and a DataTable for detail rows. Templates: activity-report.tsx bar chart of events-per-day, summary KPIs, top actions table, recent-events table (50 rows) revenue-report.tsx bar chart of revenue per stage, breakdown table with totals row, currency-aware formatting pipeline-report.tsx funnel chart of interests per stage, top interests table, win rate / cycle KPIs occupancy-report.tsx donut pie of berth status mix, status breakdown table with percentages, occupancy rate KPI reports.service.tsx (renamed .ts -> .tsx for JSX): - swap REPORT_TYPE_MAP `template`/`buildInputs` for a single `render` function returning a typed react-pdf element - inject port logo via resolvePortLogo() and pass through to every template through a ReportContext object - keep the existing job queue / storage / file-row / socket-emit flow intact — only the inner PDF-bytes generation changed Old pdfme files deleted (4 templates). buildStoragePath / files-table insert / notifications / status updates all unchanged. Tests: tests/unit/report-templates.test.tsx (5 tests): each report renders to valid PDF bytes given a representative seed-style fixture; empty data path doesn't throw. 1313/1313 vitest green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:55:07 +02:00
import {
BarChart,
DataTable,
DocumentShell,
KeyValueGrid,
Section,
type BarDatum,
} from '@/lib/pdf/brand-kit';
import { stageLabel } from '@/lib/constants';
import type { RevenueData } from '@/lib/services/report-generators';
export interface RevenueReportPdfProps {
portName: string;
logoBuffer: Buffer | null;
data: RevenueData;
dateFrom?: string;
dateTo?: string;
currency?: string;
}
const STAGE_ORDER = [
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
'enquiry',
'qualified',
'nurturing',
'eoi',
'reservation',
'deposit_paid',
'contract',
feat(reports): migrate 4 reports from pdfme to react-pdf Phase 1 / commits 3-6 of 14 — bundled because every report follows the same conversion pattern (coordinate-stuffed pdfme template -> JSX brand kit). Each report now has a real header (logo + port name), structured KeyValueGrid for summary stats, a chart (BarChart / FunnelChart / PieChart / LineChart-ready), and a DataTable for detail rows. Templates: activity-report.tsx bar chart of events-per-day, summary KPIs, top actions table, recent-events table (50 rows) revenue-report.tsx bar chart of revenue per stage, breakdown table with totals row, currency-aware formatting pipeline-report.tsx funnel chart of interests per stage, top interests table, win rate / cycle KPIs occupancy-report.tsx donut pie of berth status mix, status breakdown table with percentages, occupancy rate KPI reports.service.tsx (renamed .ts -> .tsx for JSX): - swap REPORT_TYPE_MAP `template`/`buildInputs` for a single `render` function returning a typed react-pdf element - inject port logo via resolvePortLogo() and pass through to every template through a ReportContext object - keep the existing job queue / storage / file-row / socket-emit flow intact — only the inner PDF-bytes generation changed Old pdfme files deleted (4 templates). buildStoragePath / files-table insert / notifications / status updates all unchanged. Tests: tests/unit/report-templates.test.tsx (5 tests): each report renders to valid PDF bytes given a representative seed-style fixture; empty data path doesn't throw. 1313/1313 vitest green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:55:07 +02:00
];
function fmtAmount(n: number, currency: string): string {
return `${currency} ${n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
export function RevenueReportPdf({
portName,
logoBuffer,
data,
dateFrom,
dateTo,
currency = 'USD',
}: RevenueReportPdfProps) {
const stages = [
...STAGE_ORDER.filter((s) => data.stageRevenue[s] !== undefined),
...Object.keys(data.stageRevenue).filter((s) => !STAGE_ORDER.includes(s)),
];
const rows = stages.map((stage) => ({
stage,
amount: Number(data.stageRevenue[stage] ?? 0),
}));
const chartData: BarDatum[] = rows.map((r) => ({ label: stageLabel(r.stage), value: r.amount }));
const total = Number(data.totalCompleted);
const subtotal = rows.reduce((s, r) => s + r.amount, 0);
const meta = dateFrom || dateTo ? `Range: ${dateFrom ?? '—'}${dateTo ?? 'today'}` : 'All time';
return (
<DocumentShell
portName={portName}
docTitle="Revenue Report"
docMeta={meta}
logoBuffer={logoBuffer}
>
<Section title="Summary">
<KeyValueGrid
rows={[
{ label: 'Total completed', value: fmtAmount(total, currency) },
{ label: 'Pipeline value (open)', value: fmtAmount(subtotal - total, currency) },
{ label: 'Total stages', value: rows.length },
{ label: 'Currency', value: currency },
]}
/>
</Section>
<Section
title="Revenue by stage"
subtitle="Sum of expected berth value at each pipeline stage."
>
<BarChart data={chartData} height={220} showValues />
</Section>
<Section title="Breakdown">
<DataTable<{ stage: string; amount: number }>
columns={[
{ header: 'Stage', flex: 3, render: (r) => stageLabel(r.stage) },
{
header: 'Amount',
flex: 2,
align: 'right',
render: (r) => fmtAmount(r.amount, currency),
},
]}
rows={rows}
totals={['Total', fmtAmount(subtotal, currency)]}
emptyMessage="No revenue data."
/>
</Section>
</DocumentShell>
);
}