frontend phase 9
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { Divider, Paper, Stack, Typography } from '@mui/material';
|
||||
import { formatIrrToToman, parseIrr } from '@/utils';
|
||||
|
||||
export interface PriceBreakdownRow {
|
||||
/** Stable row key (e.g. `service_cost`) — also exposed as `data-row` for tests/automation. */
|
||||
key: string;
|
||||
/** Display label — already translated by the caller (labels are i18n keys, never derived from codes). */
|
||||
label: string;
|
||||
/** IRR digit-string, straight off the wire. */
|
||||
amountIrr: string;
|
||||
}
|
||||
|
||||
export interface PriceBreakdownProps {
|
||||
rows: PriceBreakdownRow[];
|
||||
totalLabel: string;
|
||||
/** IRR digit-string. Must equal the integer sum of `rows` — see the reconciliation guard below. */
|
||||
totalAmountIrr: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The reconciling money breakdown (C6 checkout, invoice; f10/f11 reuse it for refunds/BNPL). Rows and
|
||||
* total are **served amounts** — this component only formats (BigInt-safe, Toman display via the money
|
||||
* util) and never computes a figure. The one thing it enforces is the phase's hard rule: the displayed
|
||||
* rows must sum to the displayed total. A mismatch is a data bug upstream, so it is surfaced loudly in
|
||||
* dev (console.error) rather than silently rendered.
|
||||
* @component PriceBreakdown
|
||||
*/
|
||||
const PriceBreakdown: FunctionComponent<PriceBreakdownProps> = ({ rows, totalLabel, totalAmountIrr }) => {
|
||||
const locale = useLocale();
|
||||
const tc = useTranslations('common');
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const sum = rows.reduce((acc, row) => acc + parseIrr(row.amountIrr), BigInt(0));
|
||||
if (sum !== parseIrr(totalAmountIrr)) {
|
||||
console.error(
|
||||
`PriceBreakdown: rows sum to ${sum} but total is ${totalAmountIrr} — a breakdown must reconcile to the rial.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper elevation={0} sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}>
|
||||
<Stack sx={{ gap: 1.25 }}>
|
||||
{rows.map((row) => (
|
||||
<Stack key={row.key} data-row={row.key} direction="row" sx={{ justifyContent: 'space-between', gap: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{row.label}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{formatIrrToToman(row.amountIrr, locale)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
<Divider />
|
||||
<Stack data-row="total" direction="row" sx={{ justifyContent: 'space-between', gap: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>
|
||||
{totalLabel}
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: 'var(--bal-secondary)' }}>
|
||||
{formatIrrToToman(totalAmountIrr, locale)} {tc('currency_toman')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PriceBreakdown;
|
||||
Reference in New Issue
Block a user