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:
2026-05-09 18:35:34 +02:00
parent 7804e9bb17
commit 43191659e6
10 changed files with 53 additions and 71 deletions

View File

@@ -56,9 +56,7 @@ export function BerthCard({ berth }: BerthCardProps) {
const metaParts: string[] = [];
if (dimText) metaParts.push(dimText);
if (berth.price)
metaParts.push(
formatCurrency(berth.price, berth.priceCurrency, { maxFractionDigits: 0 }),
);
metaParts.push(formatCurrency(berth.price, berth.priceCurrency, { maxFractionDigits: 0 }));
const tags = berth.tags ?? [];

View File

@@ -405,18 +405,14 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
<CurrencyInput
value={watch('price') ?? ''}
currency={watch('priceCurrency') ?? 'USD'}
onChange={(v) =>
setValue('price', v ?? undefined, { shouldDirty: true })
}
onChange={(v) => setValue('price', v ?? undefined, { shouldDirty: true })}
/>
</div>
<div className="space-y-2">
<Label>Currency</Label>
<CurrencySelect
value={watch('priceCurrency') ?? 'USD'}
onValueChange={(v) =>
setValue('priceCurrency', v, { shouldDirty: true })
}
onValueChange={(v) => setValue('priceCurrency', v, { shouldDirty: true })}
/>
</div>
</div>

View File

@@ -7,6 +7,7 @@ import { EmptyState } from '@/components/shared/empty-state';
import { ChartCard } from './chart-card';
import { useRevenue } from './use-analytics';
import type { DateRange } from '@/lib/services/analytics.service';
import { formatCurrency } from '@/lib/utils/currency';
interface Props {
range: DateRange;
@@ -71,9 +72,9 @@ export function RevenueBreakdownChart({ range }: Props) {
fontSize: 12,
}}
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);
return [`${num.toLocaleString()} ${c}`, 'Amount'];
return [formatCurrency(num, c), 'Amount'];
}}
/>
<Bar dataKey="amount" fill="hsl(var(--chart-3))" radius={[4, 4, 0, 0]} />

View File

@@ -10,6 +10,7 @@ import { format } from 'date-fns';
import { Button } from '@/components/ui/button';
import { apiFetch } from '@/lib/api/client';
import { cn } from '@/lib/utils';
import { formatCurrency } from '@/lib/utils/currency';
import type { ExpenseRow } from './expense-columns';
interface Props {
@@ -59,9 +60,10 @@ export function ExpenseDuplicateBanner({ expense }: Props) {
if (!expense.duplicateOf) return null;
const candidateLabel = candidate
? `${candidate.establishmentName ?? 'Unnamed expense'} · ${
candidate.amount
} ${candidate.currency} · ${format(new Date(candidate.expenseDate), 'd MMM yyyy')}`
? `${candidate.establishmentName ?? 'Unnamed expense'} · ${formatCurrency(
candidate.amount,
candidate.currency,
)} · ${format(new Date(candidate.expenseDate), 'd MMM yyyy')}`
: 'a previously recorded expense';
return (

View File

@@ -112,9 +112,7 @@ export function InvoiceLineItems({ name = 'lineItems', currency = 'USD' }: Invoi
type="button"
variant="outline"
size="sm"
onClick={() =>
append({ description: '', quantity: 1, unitPrice: 0 })
}
onClick={() => append({ description: '', quantity: 1, unitPrice: 0 })}
>
<Plus className="mr-1.5 h-3.5 w-3.5" />
Add Line Item

View File

@@ -37,6 +37,7 @@ import {
import { apiFetch } from '@/lib/api/client';
import { cn } from '@/lib/utils';
import { formatCurrency } from '@/lib/utils/currency';
import {
useSearch,
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.status === 'sent') badges.push({ label: 'Sent', tone: 'neutral' });
const sub = inv.totalAmount
? `${inv.clientName} · ${inv.totalAmount} ${inv.currency}`
? `${inv.clientName} · ${formatCurrency(inv.totalAmount, inv.currency)}`
: inv.clientName;
rows.push({
kind: 'result',
@@ -975,7 +976,7 @@ function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
key: `expenses:${e.id}`,
bucket: 'expenses',
icon: Receipt,
label: e.description ?? e.vendor ?? `${e.amount} ${e.currency}`,
label: e.description ?? e.vendor ?? formatCurrency(e.amount, e.currency),
sub,
href: `/${portSlug}/expenses/${e.id}`,
badges: badges.length > 0 ? badges : undefined,

View File

@@ -6,8 +6,10 @@ import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { currencySymbol } from '@/lib/utils/currency';
interface CurrencyInputProps
extends Omit<React.ComponentProps<'input'>, 'value' | 'onChange' | 'type'> {
interface CurrencyInputProps extends Omit<
React.ComponentProps<'input'>,
'value' | 'onChange' | 'type'
> {
/** Controlled raw numeric value. `null` / `undefined` render empty. */
value: number | string | null | undefined;
/** 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) => {
const symbol = currencySymbol(currency);
const display =
value === null || value === undefined || value === '' ? '' : String(value);
const display = value === null || value === undefined || value === '' ? '' : String(value);
return (
<div className="relative">