frontend phase 10

This commit is contained in:
hamid
2026-07-10 12:51:53 +03:30
parent 40cc1d163b
commit ccfa27aff6
32 changed files with 2151 additions and 3 deletions
@@ -0,0 +1,113 @@
'use client';
import { FunctionComponent } from 'react';
import { useLocale, useTranslations } from 'next-intl';
import { Divider, Paper, Stack, Typography } from '@mui/material';
import PriceBreakdown from '@/components/PriceBreakdown';
import StatusChip from '@/components/StatusChip';
import AppAlert from '@/components/common/AppAlert';
import AppIcon from '@/components/common/AppIcon';
import RefundEtaBanner from '@/components/RefundEtaBanner';
import { formatShamsiDate } from '@/utils';
import type { CancellationPolicyPreview } from '@/services/refunds/types';
export interface CancellationPolicyDisclosureProps {
preview: CancellationPolicyPreview;
}
/** Percent (integer) from a 01 fraction — a small display number, never money, so JS math is safe. */
function toPercent(fraction: number): number {
return Math.round(fraction * 100);
}
/**
* The pre-confirm cancellation disclosure (f10) — the whole point of the cancel screen: the applicable
* policy tier (label off the `cancellation_policy_code` i18n key, never the raw code), the **refund % + fee
* %**, the concrete refund-vs-fee split (reusing `PriceBreakdown`, which reconciles to the rial), the
* multi-session refundable/locked breakdown, the admin-approved reality, and a per-channel ETA preview
* (BNPL surfaces the ~710-day window honestly). Display-only — the reason field, acknowledgement and
* confirm live in the page; shared so the confirm dialog and any future per-session cancel reuse it.
* @component CancellationPolicyDisclosure
*/
const CancellationPolicyDisclosure: FunctionComponent<CancellationPolicyDisclosureProps> = ({ preview }) => {
const t = useTranslations('refunds');
const locale = useLocale();
const refundPercent = toPercent(preview.refundPercentageApplied);
const feePercent = toPercent(preview.feePercentage);
const isMultiSession = preview.sessions.length > 1;
return (
<Stack sx={{ gap: 2.5 }} data-testid="cancellation-disclosure" data-policy-code={preview.cancellationPolicyCode}>
<Paper elevation={0} sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}>
<Stack sx={{ gap: 1 }}>
<Stack direction="row" sx={{ alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
{t(`policy_${preview.cancellationPolicyCode}`)}
</Typography>
<StatusChip
status={refundPercent > 0 ? 'active' : 'rejected'}
label={t('refund_percent', { percent: refundPercent })}
/>
</Stack>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t(`lead_${preview.leadTimeLabel}`)}
</Typography>
<Typography variant="body2" sx={{ color: 'var(--bal-secondary)', fontWeight: 600 }}>
{t('fee_percent', { percent: feePercent })}
</Typography>
</Stack>
</Paper>
<PriceBreakdown
rows={[
{ key: 'refund', label: t('row_refund'), amountIrr: preview.refundAmountIrr },
{ key: 'fee', label: t('row_fee'), amountIrr: preview.feeAmountIrr },
]}
totalLabel={t('row_refundable_total')}
totalAmountIrr={preview.refundableAmountIrr}
/>
{isMultiSession && (
<Paper elevation={0} sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}>
<Stack sx={{ gap: 1.25 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
{t('sessions_title')}
</Typography>
<Divider />
{preview.sessions.map((session) => (
<Stack
key={session.bookingSessionId}
data-session-id={session.bookingSessionId}
data-refundable={session.refundable}
direction="row"
sx={{ justifyContent: 'space-between', alignItems: 'center', gap: 2, opacity: session.refundable ? 1 : 0.6 }}
>
<Typography variant="body2">
{`${session.sessionIndex}. ${formatShamsiDate(session.scheduledDate, locale)}`}
</Typography>
<StatusChip
status={session.refundable ? 'active' : 'neutral'}
label={session.refundable ? t('session_refundable') : t(`reason_${session.reasonCode}`)}
/>
</Stack>
))}
</Stack>
</Paper>
)}
<RefundEtaBanner channel={preview.refundChannel} eta={preview.expectedCustomerRefundEta} />
<AppAlert
severity="info"
variant="outlined"
icon={<AppIcon icon="info" size={20} color="var(--bal-info)" />}
data-testid="admin-approval-explainer"
sx={{ marginY: 0 }}
>
{t('admin_approval_explainer')}
</AppAlert>
</Stack>
);
};
export default CancellationPolicyDisclosure;