feat(currency): sweep remaining concat call sites to formatCurrency
Builds on the centralised formatter shipped in ee2da8f. Replaces
\`\${currency} \${amount}\` style concatenations across the dashboard
revenue tooltip, command-search invoice/expense fallback labels,
expense-duplicate banner, and the invoice + expense PDF templates.
Drops the duplicate \`currencySymbol\` helper inside expense-pdf.service
in favour of the shared util; the two PDF helpers (renderReceiptHeader,
addReceiptErrorPage) now take a currency code instead of a pre-rendered
symbol so the formatter is the single source for spacing + thousands
separators. Also re-runs Prettier on the files where the prior commit
shipped without it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,9 +27,7 @@ export default async function PortalInvoicesPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">Invoices</h1>
|
<h1 className="text-2xl font-semibold text-gray-900">Invoices</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 mt-1">Your billing statements and payment history</p>
|
||||||
Your billing statements and payment history
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{invoices.length === 0 ? (
|
{invoices.length === 0 ? (
|
||||||
@@ -43,10 +41,7 @@ export default async function PortalInvoicesPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{invoices.map((invoice) => (
|
{invoices.map((invoice) => (
|
||||||
<div
|
<div key={invoice.id} className="bg-white rounded-lg border p-5">
|
||||||
key={invoice.id}
|
|
||||||
className="bg-white rounded-lg border p-5"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-4 flex-wrap">
|
<div className="flex items-start justify-between gap-4 flex-wrap">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -56,9 +56,7 @@ export function BerthCard({ berth }: BerthCardProps) {
|
|||||||
const metaParts: string[] = [];
|
const metaParts: string[] = [];
|
||||||
if (dimText) metaParts.push(dimText);
|
if (dimText) metaParts.push(dimText);
|
||||||
if (berth.price)
|
if (berth.price)
|
||||||
metaParts.push(
|
metaParts.push(formatCurrency(berth.price, berth.priceCurrency, { maxFractionDigits: 0 }));
|
||||||
formatCurrency(berth.price, berth.priceCurrency, { maxFractionDigits: 0 }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const tags = berth.tags ?? [];
|
const tags = berth.tags ?? [];
|
||||||
|
|
||||||
|
|||||||
@@ -405,18 +405,14 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
|||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
value={watch('price') ?? ''}
|
value={watch('price') ?? ''}
|
||||||
currency={watch('priceCurrency') ?? 'USD'}
|
currency={watch('priceCurrency') ?? 'USD'}
|
||||||
onChange={(v) =>
|
onChange={(v) => setValue('price', v ?? undefined, { shouldDirty: true })}
|
||||||
setValue('price', v ?? undefined, { shouldDirty: true })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Currency</Label>
|
<Label>Currency</Label>
|
||||||
<CurrencySelect
|
<CurrencySelect
|
||||||
value={watch('priceCurrency') ?? 'USD'}
|
value={watch('priceCurrency') ?? 'USD'}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) => setValue('priceCurrency', v, { shouldDirty: true })}
|
||||||
setValue('priceCurrency', v, { shouldDirty: true })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { EmptyState } from '@/components/shared/empty-state';
|
|||||||
import { ChartCard } from './chart-card';
|
import { ChartCard } from './chart-card';
|
||||||
import { useRevenue } from './use-analytics';
|
import { useRevenue } from './use-analytics';
|
||||||
import type { DateRange } from '@/lib/services/analytics.service';
|
import type { DateRange } from '@/lib/services/analytics.service';
|
||||||
|
import { formatCurrency } from '@/lib/utils/currency';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
range: DateRange;
|
range: DateRange;
|
||||||
@@ -71,9 +72,9 @@ export function RevenueBreakdownChart({ range }: Props) {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
}}
|
}}
|
||||||
formatter={(value, _name, item) => {
|
formatter={(value, _name, item) => {
|
||||||
const c = (item?.payload as { currency?: string } | undefined)?.currency ?? '';
|
const c = (item?.payload as { currency?: string } | undefined)?.currency ?? 'USD';
|
||||||
const num = typeof value === 'number' ? value : Number(value);
|
const num = typeof value === 'number' ? value : Number(value);
|
||||||
return [`${num.toLocaleString()} ${c}`, 'Amount'];
|
return [formatCurrency(num, c), 'Amount'];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="amount" fill="hsl(var(--chart-3))" radius={[4, 4, 0, 0]} />
|
<Bar dataKey="amount" fill="hsl(var(--chart-3))" radius={[4, 4, 0, 0]} />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { format } from 'date-fns';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { apiFetch } from '@/lib/api/client';
|
import { apiFetch } from '@/lib/api/client';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { formatCurrency } from '@/lib/utils/currency';
|
||||||
import type { ExpenseRow } from './expense-columns';
|
import type { ExpenseRow } from './expense-columns';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -59,9 +60,10 @@ export function ExpenseDuplicateBanner({ expense }: Props) {
|
|||||||
if (!expense.duplicateOf) return null;
|
if (!expense.duplicateOf) return null;
|
||||||
|
|
||||||
const candidateLabel = candidate
|
const candidateLabel = candidate
|
||||||
? `${candidate.establishmentName ?? 'Unnamed expense'} · ${
|
? `${candidate.establishmentName ?? 'Unnamed expense'} · ${formatCurrency(
|
||||||
candidate.amount
|
candidate.amount,
|
||||||
} ${candidate.currency} · ${format(new Date(candidate.expenseDate), 'd MMM yyyy')}`
|
candidate.currency,
|
||||||
|
)} · ${format(new Date(candidate.expenseDate), 'd MMM yyyy')}`
|
||||||
: 'a previously recorded expense';
|
: 'a previously recorded expense';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -112,9 +112,7 @@ export function InvoiceLineItems({ name = 'lineItems', currency = 'USD' }: Invoi
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() => append({ description: '', quantity: 1, unitPrice: 0 })}
|
||||||
append({ description: '', quantity: 1, unitPrice: 0 })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
||||||
Add Line Item
|
Add Line Item
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
|
|
||||||
import { apiFetch } from '@/lib/api/client';
|
import { apiFetch } from '@/lib/api/client';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { formatCurrency } from '@/lib/utils/currency';
|
||||||
import {
|
import {
|
||||||
useSearch,
|
useSearch,
|
||||||
type BucketType,
|
type BucketType,
|
||||||
@@ -951,7 +952,7 @@ function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
|
|||||||
else if (inv.paymentStatus === 'paid') badges.push({ label: 'Paid', tone: 'success' });
|
else if (inv.paymentStatus === 'paid') badges.push({ label: 'Paid', tone: 'success' });
|
||||||
else if (inv.status === 'sent') badges.push({ label: 'Sent', tone: 'neutral' });
|
else if (inv.status === 'sent') badges.push({ label: 'Sent', tone: 'neutral' });
|
||||||
const sub = inv.totalAmount
|
const sub = inv.totalAmount
|
||||||
? `${inv.clientName} · ${inv.totalAmount} ${inv.currency}`
|
? `${inv.clientName} · ${formatCurrency(inv.totalAmount, inv.currency)}`
|
||||||
: inv.clientName;
|
: inv.clientName;
|
||||||
rows.push({
|
rows.push({
|
||||||
kind: 'result',
|
kind: 'result',
|
||||||
@@ -975,7 +976,7 @@ function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
|
|||||||
key: `expenses:${e.id}`,
|
key: `expenses:${e.id}`,
|
||||||
bucket: 'expenses',
|
bucket: 'expenses',
|
||||||
icon: Receipt,
|
icon: Receipt,
|
||||||
label: e.description ?? e.vendor ?? `${e.amount} ${e.currency}`,
|
label: e.description ?? e.vendor ?? formatCurrency(e.amount, e.currency),
|
||||||
sub,
|
sub,
|
||||||
href: `/${portSlug}/expenses/${e.id}`,
|
href: `/${portSlug}/expenses/${e.id}`,
|
||||||
badges: badges.length > 0 ? badges : undefined,
|
badges: badges.length > 0 ? badges : undefined,
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { currencySymbol } from '@/lib/utils/currency';
|
import { currencySymbol } from '@/lib/utils/currency';
|
||||||
|
|
||||||
interface CurrencyInputProps
|
interface CurrencyInputProps extends Omit<
|
||||||
extends Omit<React.ComponentProps<'input'>, 'value' | 'onChange' | 'type'> {
|
React.ComponentProps<'input'>,
|
||||||
|
'value' | 'onChange' | 'type'
|
||||||
|
> {
|
||||||
/** Controlled raw numeric value. `null` / `undefined` render empty. */
|
/** Controlled raw numeric value. `null` / `undefined` render empty. */
|
||||||
value: number | string | null | undefined;
|
value: number | string | null | undefined;
|
||||||
/** Fires with a raw number (or `null` if cleared). */
|
/** Fires with a raw number (or `null` if cleared). */
|
||||||
@@ -29,8 +31,7 @@ export const CurrencyInput = React.forwardRef<HTMLInputElement, CurrencyInputPro
|
|||||||
({ value, onChange, currency = 'USD', className, ...props }, ref) => {
|
({ value, onChange, currency = 'USD', className, ...props }, ref) => {
|
||||||
const symbol = currencySymbol(currency);
|
const symbol = currencySymbol(currency);
|
||||||
|
|
||||||
const display =
|
const display = value === null || value === undefined || value === '' ? '' : String(value);
|
||||||
value === null || value === undefined || value === '' ? '' : String(value);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { Template } from '@pdfme/common';
|
import type { Template } from '@pdfme/common';
|
||||||
|
|
||||||
|
import { formatCurrency } from '@/lib/utils/currency';
|
||||||
|
|
||||||
export const invoiceTemplate: Template = {
|
export const invoiceTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as unknown as string,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
@@ -90,21 +92,22 @@ export function buildInvoiceInputs(
|
|||||||
lineItems: Record<string, unknown>[],
|
lineItems: Record<string, unknown>[],
|
||||||
port: Record<string, unknown>,
|
port: Record<string, unknown>,
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
|
const currency = (invoice.currency as string) ?? 'USD';
|
||||||
const itemLines = lineItems
|
const itemLines = lineItems
|
||||||
.map(
|
.map(
|
||||||
(li, i) =>
|
(li, i) =>
|
||||||
`${i + 1}. ${li.description} | Qty: ${li.quantity} | Unit: ${invoice.currency} ${Number(li.unitPrice).toFixed(2)} | Total: ${invoice.currency} ${Number(li.total).toFixed(2)}`,
|
`${i + 1}. ${li.description} | Qty: ${li.quantity} | Unit: ${formatCurrency(Number(li.unitPrice), currency)} | Total: ${formatCurrency(Number(li.total), currency)}`,
|
||||||
)
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
let totalsText = `Subtotal: ${invoice.currency} ${Number(invoice.subtotal).toFixed(2)}`;
|
let totalsText = `Subtotal: ${formatCurrency(Number(invoice.subtotal), currency)}`;
|
||||||
if (Number(invoice.discountAmount) > 0) {
|
if (Number(invoice.discountAmount) > 0) {
|
||||||
totalsText += `\nDiscount (${invoice.discountPct}%): -${invoice.currency} ${Number(invoice.discountAmount).toFixed(2)}`;
|
totalsText += `\nDiscount (${invoice.discountPct}%): -${formatCurrency(Number(invoice.discountAmount), currency)}`;
|
||||||
}
|
}
|
||||||
if (Number(invoice.feeAmount) > 0) {
|
if (Number(invoice.feeAmount) > 0) {
|
||||||
totalsText += `\nFee (${invoice.feePct}%): +${invoice.currency} ${Number(invoice.feeAmount).toFixed(2)}`;
|
totalsText += `\nFee (${invoice.feePct}%): +${formatCurrency(Number(invoice.feeAmount), currency)}`;
|
||||||
}
|
}
|
||||||
totalsText += `\n─────────────\nTOTAL: ${invoice.currency} ${Number(invoice.total).toFixed(2)}`;
|
totalsText += `\n─────────────\nTOTAL: ${formatCurrency(Number(invoice.total), currency)}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
portName: (port?.name as string) ?? 'Port Nimara',
|
portName: (port?.name as string) ?? 'Port Nimara',
|
||||||
@@ -112,7 +115,8 @@ export function buildInvoiceInputs(
|
|||||||
invoiceNumber: invoice.invoiceNumber as string,
|
invoiceNumber: invoice.invoiceNumber as string,
|
||||||
invoiceDate: `Date: ${new Date(invoice.createdAt as string | Date).toLocaleDateString('en-GB')}`,
|
invoiceDate: `Date: ${new Date(invoice.createdAt as string | Date).toLocaleDateString('en-GB')}`,
|
||||||
dueDate: `Due: ${invoice.dueDate}`,
|
dueDate: `Due: ${invoice.dueDate}`,
|
||||||
clientInfo: `${invoice.clientName}\n${invoice.billingEmail ?? ''}\n${invoice.billingAddress ?? ''}`.trim(),
|
clientInfo:
|
||||||
|
`${invoice.clientName}\n${invoice.billingEmail ?? ''}\n${invoice.billingAddress ?? ''}`.trim(),
|
||||||
lineItems: itemLines || 'No line items',
|
lineItems: itemLines || 'No line items',
|
||||||
totals: totalsText,
|
totals: totalsText,
|
||||||
notes: invoice.notes ? `Notes: ${invoice.notes}` : '',
|
notes: invoice.notes ? `Notes: ${invoice.notes}` : '',
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { db } from '@/lib/db';
|
|||||||
import { expenses } from '@/lib/db/schema/financial';
|
import { expenses } from '@/lib/db/schema/financial';
|
||||||
import { files } from '@/lib/db/schema/documents';
|
import { files } from '@/lib/db/schema/documents';
|
||||||
import { getRate } from '@/lib/services/currency';
|
import { getRate } from '@/lib/services/currency';
|
||||||
|
import { formatCurrency } from '@/lib/utils/currency';
|
||||||
import { getStorageBackend } from '@/lib/storage';
|
import { getStorageBackend } from '@/lib/storage';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
@@ -308,21 +309,6 @@ function pageDims(format: PageFormat): { width: number; height: number } {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Symbol helper ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function currencySymbol(c: string): string {
|
|
||||||
switch (c.toUpperCase()) {
|
|
||||||
case 'USD':
|
|
||||||
return '$';
|
|
||||||
case 'EUR':
|
|
||||||
return '€';
|
|
||||||
case 'GBP':
|
|
||||||
return '£';
|
|
||||||
default:
|
|
||||||
return c.toUpperCase() + ' ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Grouping ───────────────────────────────────────────────────────────────
|
// ─── Grouping ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function groupKey(row: ProcessedExpense, by: GroupBy): string {
|
function groupKey(row: ProcessedExpense, by: GroupBy): string {
|
||||||
@@ -574,8 +560,7 @@ function addSummaryBox(
|
|||||||
totals: Totals,
|
totals: Totals,
|
||||||
opts: { includeProcessingFee: boolean; groupBy: GroupBy },
|
opts: { includeProcessingFee: boolean; groupBy: GroupBy },
|
||||||
) {
|
) {
|
||||||
const sym = currencySymbol(totals.targetCurrency);
|
const otherCurrency = totals.targetCurrency === 'USD' ? 'EUR' : 'USD';
|
||||||
const otherSym = totals.targetCurrency === 'USD' ? '€' : '$';
|
|
||||||
const otherTotal = totals.targetCurrency === 'USD' ? totals.eurTotal : totals.usdTotal;
|
const otherTotal = totals.targetCurrency === 'USD' ? totals.eurTotal : totals.usdTotal;
|
||||||
|
|
||||||
doc.fontSize(14).font('Helvetica-Bold').text('Summary');
|
doc.fontSize(14).font('Helvetica-Bold').text('Summary');
|
||||||
@@ -584,12 +569,14 @@ function addSummaryBox(
|
|||||||
const lineY = doc.y;
|
const lineY = doc.y;
|
||||||
const lines = [
|
const lines = [
|
||||||
`Total expenses: ${totals.count}`,
|
`Total expenses: ${totals.count}`,
|
||||||
`Subtotal (${totals.targetCurrency}): ${sym}${totals.targetTotal.toFixed(2)}`,
|
`Subtotal (${totals.targetCurrency}): ${formatCurrency(totals.targetTotal, totals.targetCurrency)}`,
|
||||||
`${totals.targetCurrency === 'USD' ? 'EUR' : 'USD'} equivalent: ${otherSym}${otherTotal.toFixed(2)}`,
|
`${otherCurrency} equivalent: ${formatCurrency(otherTotal, otherCurrency)}`,
|
||||||
];
|
];
|
||||||
if (opts.includeProcessingFee) {
|
if (opts.includeProcessingFee) {
|
||||||
lines.push(`Processing fee (5%): ${sym}${totals.processingFee.toFixed(2)}`);
|
lines.push(
|
||||||
lines.push(`Final total: ${sym}${totals.finalTotal.toFixed(2)}`);
|
`Processing fee (5%): ${formatCurrency(totals.processingFee, totals.targetCurrency)}`,
|
||||||
|
);
|
||||||
|
lines.push(`Final total: ${formatCurrency(totals.finalTotal, totals.targetCurrency)}`);
|
||||||
}
|
}
|
||||||
if (opts.groupBy !== 'none') lines.push(`Grouping: by ${opts.groupBy}`);
|
if (opts.groupBy !== 'none') lines.push(`Grouping: by ${opts.groupBy}`);
|
||||||
|
|
||||||
@@ -600,7 +587,7 @@ function addSummaryBox(
|
|||||||
const warningLines = showNoReceiptWarning
|
const warningLines = showNoReceiptWarning
|
||||||
? [
|
? [
|
||||||
`WARNING: ${totals.noReceiptCount} expense${totals.noReceiptCount === 1 ? '' : 's'} on this report ${totals.noReceiptCount === 1 ? 'has' : 'have'} no receipt attached`,
|
`WARNING: ${totals.noReceiptCount} expense${totals.noReceiptCount === 1 ? '' : 's'} on this report ${totals.noReceiptCount === 1 ? 'has' : 'have'} no receipt attached`,
|
||||||
`(${sym}${totals.noReceiptAmount.toFixed(2)} at risk of being denied reimbursement).`,
|
`(${formatCurrency(totals.noReceiptAmount, totals.targetCurrency)} at risk of being denied reimbursement).`,
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -663,7 +650,6 @@ function addExpenseTable(
|
|||||||
doc.fontSize(14).font('Helvetica-Bold').text('Expense details');
|
doc.fontSize(14).font('Helvetica-Bold').text('Expense details');
|
||||||
doc.moveDown(0.4);
|
doc.moveDown(0.4);
|
||||||
|
|
||||||
const sym = currencySymbol(opts.targetCurrency);
|
|
||||||
const baseColumns: Column[] = [
|
const baseColumns: Column[] = [
|
||||||
{ header: 'Date', width: 60, x: 60 },
|
{ header: 'Date', width: 60, x: 60 },
|
||||||
{ header: 'Establishment', width: 110, x: 120 },
|
{ header: 'Establishment', width: 110, x: 120 },
|
||||||
@@ -707,7 +693,7 @@ function addExpenseTable(
|
|||||||
}
|
}
|
||||||
doc.fillColor('#000000').fontSize(8).font('Helvetica');
|
doc.fillColor('#000000').fontSize(8).font('Helvetica');
|
||||||
const date = row.expenseDate.toISOString().slice(0, 10);
|
const date = row.expenseDate.toISOString().slice(0, 10);
|
||||||
const amount = `${sym}${row.amountTarget.toFixed(2)}`;
|
const amount = formatCurrency(row.amountTarget, opts.targetCurrency);
|
||||||
// Annotate the establishment cell with a red "(no receipt)" marker
|
// Annotate the establishment cell with a red "(no receipt)" marker
|
||||||
// when the rep created the expense without proof. This keeps the
|
// when the rep created the expense without proof. This keeps the
|
||||||
// warning glanceable per row without adding a new column.
|
// warning glanceable per row without adding a new column.
|
||||||
@@ -760,7 +746,7 @@ function addExpenseTable(
|
|||||||
.fontSize(9)
|
.fontSize(9)
|
||||||
.font('Helvetica-Bold')
|
.font('Helvetica-Bold')
|
||||||
.text(
|
.text(
|
||||||
`${group.key} (${group.rows.length} expense${group.rows.length === 1 ? '' : 's'} — ${sym}${groupTotal.toFixed(2)})`,
|
`${group.key} (${group.rows.length} expense${group.rows.length === 1 ? '' : 's'} — ${formatCurrency(groupTotal, opts.targetCurrency)})`,
|
||||||
65,
|
65,
|
||||||
doc.y + 5,
|
doc.y + 5,
|
||||||
{ width: doc.page.width - 130 },
|
{ width: doc.page.width - 130 },
|
||||||
@@ -791,7 +777,6 @@ async function addReceiptPages(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const backend = await getStorageBackend();
|
const backend = await getStorageBackend();
|
||||||
const sym = currencySymbol(opts.targetCurrency);
|
|
||||||
|
|
||||||
let receiptCounter = 0;
|
let receiptCounter = 0;
|
||||||
let resizedCount = 0;
|
let resizedCount = 0;
|
||||||
@@ -818,7 +803,7 @@ async function addReceiptPages(
|
|||||||
expense,
|
expense,
|
||||||
receiptCounter,
|
receiptCounter,
|
||||||
totalReceipts,
|
totalReceipts,
|
||||||
sym,
|
opts.targetCurrency,
|
||||||
'Receipt file metadata missing',
|
'Receipt file metadata missing',
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -837,7 +822,7 @@ async function addReceiptPages(
|
|||||||
|
|
||||||
// Page header
|
// Page header
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
renderReceiptHeader(doc, expense, file, receiptCounter, totalReceipts, sym);
|
renderReceiptHeader(doc, expense, file, receiptCounter, totalReceipts, opts.targetCurrency);
|
||||||
|
|
||||||
// Embed the image full-bleed in the remaining vertical space.
|
// Embed the image full-bleed in the remaining vertical space.
|
||||||
const margin = 60;
|
const margin = 60;
|
||||||
@@ -881,7 +866,7 @@ async function addReceiptPages(
|
|||||||
expense,
|
expense,
|
||||||
receiptCounter,
|
receiptCounter,
|
||||||
totalReceipts,
|
totalReceipts,
|
||||||
sym,
|
opts.targetCurrency,
|
||||||
(err as Error).message ?? 'Receipt could not be loaded from storage',
|
(err as Error).message ?? 'Receipt could not be loaded from storage',
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -909,7 +894,7 @@ function renderReceiptHeader(
|
|||||||
file: ResolvedFile,
|
file: ResolvedFile,
|
||||||
index: number,
|
index: number,
|
||||||
total: number,
|
total: number,
|
||||||
sym: string,
|
currency: string,
|
||||||
) {
|
) {
|
||||||
const margin = 60;
|
const margin = 60;
|
||||||
const headerH = 90;
|
const headerH = 90;
|
||||||
@@ -936,7 +921,7 @@ function renderReceiptHeader(
|
|||||||
.fontSize(11)
|
.fontSize(11)
|
||||||
.font('Helvetica-Bold')
|
.font('Helvetica-Bold')
|
||||||
.text(
|
.text(
|
||||||
`${expense.establishmentName ?? '—'} ${sym}${expense.amountTarget.toFixed(2)}`,
|
`${expense.establishmentName ?? '—'} ${formatCurrency(expense.amountTarget, currency)}`,
|
||||||
margin + 10,
|
margin + 10,
|
||||||
baseY + 36,
|
baseY + 36,
|
||||||
);
|
);
|
||||||
@@ -962,7 +947,7 @@ function addReceiptErrorPage(
|
|||||||
expense: ProcessedExpense,
|
expense: ProcessedExpense,
|
||||||
index: number,
|
index: number,
|
||||||
total: number,
|
total: number,
|
||||||
sym: string,
|
currency: string,
|
||||||
message: string,
|
message: string,
|
||||||
) {
|
) {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
@@ -970,9 +955,10 @@ function addReceiptErrorPage(
|
|||||||
doc
|
doc
|
||||||
.fontSize(11)
|
.fontSize(11)
|
||||||
.font('Helvetica')
|
.font('Helvetica')
|
||||||
.text(`${expense.establishmentName ?? '—'} ${sym}${expense.amountTarget.toFixed(2)}`, {
|
.text(
|
||||||
align: 'center',
|
`${expense.establishmentName ?? '—'} ${formatCurrency(expense.amountTarget, currency)}`,
|
||||||
});
|
{ align: 'center' },
|
||||||
|
);
|
||||||
doc.moveDown(2);
|
doc.moveDown(2);
|
||||||
doc.fontSize(11).fillColor('#dc3545').text(message, { align: 'center' });
|
doc.fontSize(11).fillColor('#dc3545').text(message, { align: 'center' });
|
||||||
doc.fillColor('#000000');
|
doc.fillColor('#000000');
|
||||||
|
|||||||
Reference in New Issue
Block a user