frontend phase 11
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
'use client';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { Box, Paper, Skeleton, Stack, Typography } from '@mui/material';
|
||||
import { AppButton, AppIcon, InstallmentScheduleRow, PlaceholderScreen } from '@/components';
|
||||
import { formatIrrToToman, formatShamsiDate } from '@/utils';
|
||||
import { useWalletInstallments } from '@/services/bnpl';
|
||||
import type { WalletInstallmentPlan } from '@/services/bnpl/types';
|
||||
|
||||
/**
|
||||
* D5 · پیگیری اقساط — the Wallet view of active installment plans. It reads `useWalletInstallments` and
|
||||
* renders **provider-reported** status: an outstanding-balance card (terracotta), the next-installment
|
||||
* date + a provider hand-off «پرداخت زودهنگام» (early-pay is a *provider* action, never a Balinyaar
|
||||
* transaction), the per-installment due list with status chips, and the ownership note (Balinyaar displays,
|
||||
* it does not manage, this schedule). Self-contained under the Wallet route so f12 nurse-earnings content
|
||||
* can land beside it later.
|
||||
*/
|
||||
const WalletInstallments: FunctionComponent = () => {
|
||||
const t = useTranslations('bnpl');
|
||||
const tc = useTranslations('common');
|
||||
const { data: plans, isLoading, isError, refetch } = useWalletInstallments();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2, maxWidth: 560, mx: 'auto', width: '100%' }}>
|
||||
<Typography variant="h6" component="h1">
|
||||
{t('wallet_title')}
|
||||
</Typography>
|
||||
|
||||
{isLoading ? (
|
||||
<Stack sx={{ gap: 1.5 }}>
|
||||
<Skeleton variant="rounded" height={128} />
|
||||
<Skeleton variant="rounded" height={56} />
|
||||
<Skeleton variant="rounded" height={56} />
|
||||
</Stack>
|
||||
) : isError ? (
|
||||
<Paper elevation={0} sx={{ p: 3, borderRadius: 2, border: '1px solid', borderColor: 'divider', textAlign: 'center' }}>
|
||||
<Stack sx={{ gap: 1, alignItems: 'center' }}>
|
||||
<AppIcon icon="warning" size={36} color="var(--bal-warning)" />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||
{t('wallet_error_title')}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{t('wallet_error_body')}
|
||||
</Typography>
|
||||
<AppButton variant="outlined" color="secondary" onClick={() => refetch()} sx={{ m: 0, mt: 1 }}>
|
||||
{tc('retry')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : !plans || plans.length === 0 ? (
|
||||
<PlaceholderScreen icon="installments" title={t('wallet_empty_title')} description={t('wallet_empty_body')} />
|
||||
) : (
|
||||
<Stack sx={{ gap: 3 }}>
|
||||
{plans.map((plan) => (
|
||||
<InstallmentPlanSection key={plan.bnplTransactionId} plan={plan} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
function InstallmentPlanSection({ plan }: { plan: WalletInstallmentPlan }) {
|
||||
const t = useTranslations('bnpl');
|
||||
const tc = useTranslations('common');
|
||||
const locale = useLocale();
|
||||
const providerName = t(`provider_${plan.providerCode}`);
|
||||
|
||||
const handleEarlyPay = () => {
|
||||
// Early-pay is a PROVIDER action — hand off to the provider, never a Balinyaar payment.
|
||||
if (plan.earlyPayUrl) window.open(plan.earlyPayUrl, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 1.5 }}>
|
||||
{/* Outstanding-balance card — terracotta financial accent; contrast text is scheme-stable. */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{ p: 2.25, borderRadius: 3, backgroundColor: 'var(--bal-secondary)', color: 'var(--bal-secondary-contrast)' }}
|
||||
>
|
||||
<Typography variant="caption" sx={{ opacity: 0.85 }}>
|
||||
{t('outstanding_balance')}
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800, mt: 0.25 }}>
|
||||
{formatIrrToToman(plan.outstandingBalanceIrr, locale)} {tc('currency_toman')}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.85, display: 'block', mt: 0.5 }}>
|
||||
{plan.serviceLabel}
|
||||
</Typography>
|
||||
|
||||
{plan.nextDueDate ? (
|
||||
<Stack direction="row" sx={{ justifyContent: 'space-between', alignItems: 'flex-end', mt: 1.5, gap: 1.5 }}>
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ opacity: 0.85 }}>
|
||||
{t('next_installment')} · {formatShamsiDate(`${plan.nextDueDate}T00:00:00`, locale)}
|
||||
</Typography>
|
||||
{plan.nextAmountIrr ? (
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>
|
||||
{formatIrrToToman(plan.nextAmountIrr, locale)} {tc('currency_toman')}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
{plan.earlyPayUrl ? (
|
||||
<AppButton
|
||||
color="primary"
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleEarlyPay}
|
||||
sx={{ m: 0, flex: 'none' }}
|
||||
>
|
||||
{t('early_pay')}
|
||||
</AppButton>
|
||||
) : null}
|
||||
</Stack>
|
||||
) : null}
|
||||
</Paper>
|
||||
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||
{t('due_dates')}
|
||||
</Typography>
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
{plan.installments.map((row) => (
|
||||
<InstallmentScheduleRow key={`${row.kind}-${row.sequence}`} row={row} showStatus />
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* Ownership note: Balinyaar displays, it does not manage, this provider-owned schedule. */}
|
||||
<Stack direction="row" sx={{ gap: 0.75, alignItems: 'flex-start', px: 0.5 }}>
|
||||
<AppIcon icon="info" size={16} color="var(--bal-text-secondary)" />
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
{t('provider_owned_note', { provider: providerName })}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default WalletInstallments;
|
||||
@@ -1,8 +1,10 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { PlaceholderScreen } from '@/components';
|
||||
import WalletInstallments from './WalletInstallments';
|
||||
|
||||
export default async function WalletPage() {
|
||||
const t = await getTranslations('nav');
|
||||
const tShell = await getTranslations('shell');
|
||||
return <PlaceholderScreen icon="wallet" title={t('wallet')} description={tShell('placeholder_body')} />;
|
||||
/**
|
||||
* /wallet — the customer Wallet tab. Today it hosts the f11 D5 installment-status section (provider-reported,
|
||||
* self-contained so the f12 nurse-earnings Wallet content can land beside it later). The section is a client
|
||||
* component (TanStack Query); this page is the thin route shell.
|
||||
*/
|
||||
export default function WalletPage() {
|
||||
return <WalletInstallments />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user