102 lines
2.7 KiB
TypeScript
102 lines
2.7 KiB
TypeScript
|
|
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 = [
|
||
|
|
'open',
|
||
|
|
'details_sent',
|
||
|
|
'in_communication',
|
||
|
|
'eoi_sent',
|
||
|
|
'eoi_signed',
|
||
|
|
'deposit_10pct',
|
||
|
|
'contract_sent',
|
||
|
|
'contract_signed',
|
||
|
|
'completed',
|
||
|
|
];
|
||
|
|
|
||
|
|
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>
|
||
|
|
);
|
||
|
|
}
|