ui phase 8

This commit is contained in:
hamid
2026-07-19 13:57:11 +03:30
parent edc38543fd
commit 1ef4feb911
41 changed files with 2113 additions and 667 deletions
@@ -3,21 +3,26 @@ import { useState } from 'react';
import { useTranslations } from 'next-intl';
import { useSnackbar } from 'notistack';
import { Box, Stack, TextField, Typography } from '@mui/material';
import { AppButton, AppLoading, BankStatusPanel, EmptyState } from '@/components';
import { AppButton, AppLoading, BankStatusPanel, EmptyState, ErrorState } from '@/components';
import { useNurseBankAccounts, useAddNurseBankAccount, useSetPrimaryBankAccount } from '@/services/nurse';
import { isValidSheba } from '@/services/nurse/iban';
import { deriveBankStatus } from '@/services/nurse/types';
/**
* Nurse payout bank settings — submit an IBAN (شبا) + account-holder name, then watch the
* ownership inquiry resolve through its three states (pending → verified / mismatch). The list
* polls only while pending; a verified account shows the masked IBAN; mismatch offers re-enter.
* Nurse payout bank settings — an **accounts section**, not a one-shot form (ui-phase-8 §3.7): submit
* an IBAN (شبا) + account-holder name, then watch the ownership inquiry resolve through its three
* states (pending verified / mismatch, `BankStatusPanel` unchanged). Once at least one account
* exists, a persistent «افزودن حساب دیگر» CTA replaces the old form-only-when-empty gate, so a nurse
* switching banks is never dead-ended — the old account stays listed until the nurse makes the new
* one primary. A failed accounts query renders the error state with retry, **never** the empty-state
* form (which would invite a duplicate-IBAN submission blind).
*/
export default function NurseBankPage() {
const t = useTranslations('bank');
const tc = useTranslations('common');
const { enqueueSnackbar } = useSnackbar();
const { data, isLoading } = useNurseBankAccounts();
const { data, isLoading, isError, refetch } = useNurseBankAccounts();
const addAccount = useAddNurseBankAccount();
const setPrimary = useSetPrimaryBankAccount();
@@ -28,7 +33,7 @@ export default function NurseBankPage() {
const [showForm, setShowForm] = useState(false);
const accounts = data ?? [];
const showFormNow = !isLoading && (accounts.length === 0 || showForm);
const showFormNow = !isLoading && !isError && (accounts.length === 0 || showForm);
const submit = () => {
const ibanInvalid = !isValidSheba(iban);
@@ -64,47 +69,67 @@ export default function NurseBankPage() {
{isLoading ? <AppLoading /> : null}
{accounts.map((account) => {
const status = deriveBankStatus(account);
return (
<Stack key={account.id} sx={{ gap: 1 }}>
<BankStatusPanel
status={status}
chipLabel={t(`status_${status}_chip`)}
title={t(`status_${status}_title`)}
body={t(`status_${status}_body`)}
ibanMasked={status === 'verified' ? account.ibanMasked : undefined}
ibanLabel={t('iban_masked_label')}
bankName={account.bankName || undefined}
isPrimary={account.isPrimary}
primaryLabel={t('primary')}
onReenter={status === 'mismatch' ? () => setShowForm(true) : undefined}
reenterLabel={t('reenter')}
/>
{/* Promote a verified non-primary account so payouts (gated on matchedNationalId) target it. */}
{status === 'verified' && !account.isPrimary ? (
<AppButton
variant="text"
color="primary"
disabled={setPrimary.isPending}
onClick={() =>
setPrimary.mutate(account.id, {
onSuccess: () => enqueueSnackbar(t('primary_set'), { variant: 'success' }),
})
}
sx={{ alignSelf: 'flex-start' }}
>
{t('make_primary')}
</AppButton>
) : null}
</Stack>
);
})}
{isError ? <ErrorState message={t('load_error')} retryLabel={tc('retry')} onRetry={() => refetch()} /> : null}
{!isLoading && accounts.length === 0 ? (
{!isError
? accounts.map((account) => {
const status = deriveBankStatus(account);
return (
<Stack key={account.id} sx={{ gap: 1 }}>
<BankStatusPanel
status={status}
chipLabel={t(`status_${status}_chip`)}
title={t(`status_${status}_title`)}
body={t(`status_${status}_body`)}
ibanMasked={status === 'verified' ? account.ibanMasked : undefined}
ibanLabel={t('iban_masked_label')}
bankName={account.bankName || undefined}
isPrimary={account.isPrimary}
primaryLabel={t('primary')}
onReenter={status === 'mismatch' ? () => setShowForm(true) : undefined}
reenterLabel={t('reenter')}
/>
{/* Promote a verified non-primary account so payouts (gated on matchedNationalId) target it. */}
{status === 'verified' && !account.isPrimary ? (
<AppButton
variant="text"
color="primary"
disabled={setPrimary.isPending}
onClick={() =>
setPrimary.mutate(account.id, {
onSuccess: () => enqueueSnackbar(t('primary_set'), { variant: 'success' }),
onError: () => enqueueSnackbar(t('primary_set_error'), { variant: 'error' }),
})
}
sx={{ alignSelf: 'flex-start' }}
>
{t('make_primary')}
</AppButton>
) : null}
</Stack>
);
})
: null}
{!isLoading && !isError && accounts.length === 0 ? (
<EmptyState icon="bank" title={t('empty_title')} body={t('empty_body')} />
) : null}
{/* An accounts section, not a one-shot form: once at least one account exists, a persistent CTA
(rather than "no account yet") lets a nurse switching banks add another — the old account
stays listed until they make the new one primary. */}
{!isLoading && !isError && accounts.length > 0 && !showForm ? (
<AppButton
variant="outlined"
color="primary"
startIcon="add"
onClick={() => setShowForm(true)}
sx={{ alignSelf: 'flex-start' }}
>
{t('add_another')}
</AppButton>
) : null}
{showFormNow ? (
<Stack sx={{ gap: 2 }}>
<TextField
@@ -130,16 +155,26 @@ export default function NurseBankPage() {
helperText={holderError ? t('holder_required') : t('holder_hint')}
fullWidth
/>
<AppButton
color="primary"
variant="contained"
startIcon="bank"
onClick={submit}
disabled={addAccount.isPending}
sx={{ alignSelf: 'flex-start' }}
>
{addAccount.isPending ? t('submitting') : t('submit')}
</AppButton>
<Stack direction="row" sx={{ gap: 1 }}>
<AppButton color="primary" variant="contained" startIcon="bank" onClick={submit} disabled={addAccount.isPending}>
{addAccount.isPending ? t('submitting') : t('submit')}
</AppButton>
{accounts.length > 0 ? (
<AppButton
variant="text"
onClick={() => {
setShowForm(false);
setIban('');
setHolder('');
setIbanError(false);
setHolderError(false);
}}
disabled={addAccount.isPending}
>
{tc('cancel')}
</AppButton>
) : null}
</Stack>
</Stack>
) : null}
</Box>