feat(expense-export): parent-company react-pdf + pdfkit brand header
Phase 1 / commit 10 of 14 — migrates the pdfme-based parent-company
expense export to react-pdf and adds a shared brand header to the
pdfkit-based streaming expense PDF so both surfaces match the rest of
the internal-only PDF family.
parent-company-expense.tsx:
Summary KV grid (entry count, subtotal, fee, total) + entries table
with right-aligned EUR amounts and a totals row. Footnote rendered
when the EUR rate lookup falls through to the 1:1 USD:EUR fallback.
expense-export.tsx (renamed .ts -> .tsx):
- exportParentCompany now renders the react-pdf template via
resolvePortLogo() + renderPdf()
- dropped the inline pdfme template object (was the last pdfme caller
in this file)
- return type widened from Uint8Array to Buffer; caller already wraps
in Buffer.from() so no API change downstream
expense-pdf.service.ts (the pdfkit streaming engine — unchanged):
- addHeader() now draws a dark slate band matching the brand-kit
header band, with the port logo letterboxed on the left and the
document title right-aligned. Falls back to text port-name if the
logo image is missing or can't be decoded by pdfkit
- port + logo resolved once per export via Promise.all
- subheader stays beneath the band in muted grey, same as before
- streaming behavior + receipt embedding + sharp compression
untouched — the only change is the visual treatment of the header
Old pdfme inline template deleted along with the generatePdf import.
After this commit, the only remaining pdfme imports are in:
invoice-template.ts, tiptap-to-pdfme.ts, eoi-standard-inapp.ts, and
document-templates.ts (lines 516-522). All four are removed in
commits 11-12.
1319/1319 vitest green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
src/lib/pdf/templates/parent-company-expense.tsx
Normal file
91
src/lib/pdf/templates/parent-company-expense.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { DataTable, DocumentShell, KeyValueGrid, Section } from '@/lib/pdf/brand-kit';
|
||||
|
||||
export interface ParentCompanyRow {
|
||||
date: string;
|
||||
establishment: string;
|
||||
category: string;
|
||||
amountEur: number;
|
||||
}
|
||||
|
||||
export interface ParentCompanyExpensePdfProps {
|
||||
portName: string;
|
||||
logoBuffer: Buffer | null;
|
||||
rows: ParentCompanyRow[];
|
||||
subtotal: number;
|
||||
managementFee: number;
|
||||
total: number;
|
||||
/** Optional ISO date range for the meta line. */
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
/** Whether the EUR rate lookup succeeded. Renders a small footnote if false. */
|
||||
rateAvailable?: boolean;
|
||||
}
|
||||
|
||||
function fmtEur(n: number): string {
|
||||
return `EUR ${n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
export function ParentCompanyExpensePdf({
|
||||
portName,
|
||||
logoBuffer,
|
||||
rows,
|
||||
subtotal,
|
||||
managementFee,
|
||||
total,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
rateAvailable = true,
|
||||
}: ParentCompanyExpensePdfProps) {
|
||||
const meta =
|
||||
dateFrom || dateTo
|
||||
? `Range: ${dateFrom ?? '—'} → ${dateTo ?? 'today'} · ${rows.length} entries`
|
||||
: `${rows.length} entries`;
|
||||
|
||||
return (
|
||||
<DocumentShell
|
||||
portName={portName}
|
||||
docTitle="Parent Company Expense Report (EUR)"
|
||||
docMeta={meta}
|
||||
logoBuffer={logoBuffer}
|
||||
>
|
||||
<Section title="Summary">
|
||||
<KeyValueGrid
|
||||
rows={[
|
||||
{ label: 'Entries', value: rows.length },
|
||||
{ label: 'Subtotal', value: fmtEur(subtotal) },
|
||||
{ label: 'Management fee (5%)', value: fmtEur(managementFee) },
|
||||
{ label: 'Total', value: fmtEur(total) },
|
||||
]}
|
||||
/>
|
||||
{!rateAvailable ? (
|
||||
<Section title="Note">
|
||||
<KeyValueGrid
|
||||
rows={[
|
||||
{
|
||||
label: 'Currency conversion',
|
||||
value:
|
||||
'EUR exchange rate unavailable at generation time — amounts shown at 1:1 USD:EUR fallback.',
|
||||
},
|
||||
]}
|
||||
layout="stacked"
|
||||
/>
|
||||
</Section>
|
||||
) : null}
|
||||
</Section>
|
||||
|
||||
<Section title="Entries">
|
||||
<DataTable<ParentCompanyRow>
|
||||
columns={[
|
||||
{ header: 'Date', flex: 1, render: (r) => r.date },
|
||||
{ header: 'Establishment', flex: 3, render: (r) => r.establishment },
|
||||
{ header: 'Category', flex: 2, render: (r) => r.category },
|
||||
{ header: 'Amount', flex: 2, align: 'right', render: (r) => fmtEur(r.amountEur) },
|
||||
]}
|
||||
rows={rows}
|
||||
totals={['Total', '', '', fmtEur(total)]}
|
||||
emptyMessage="No expenses in the selected range."
|
||||
/>
|
||||
</Section>
|
||||
</DocumentShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user