frontend phase 9

This commit is contained in:
hamid
2026-07-10 11:49:55 +03:30
parent cd6c2591a6
commit 40cc1d163b
49 changed files with 4130 additions and 20 deletions
@@ -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;