172 lines
6.9 KiB
TypeScript
172 lines
6.9 KiB
TypeScript
'use client';
|
|
import { useState } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { FormProvider, useForm } from 'react-hook-form';
|
|
import { useSnackbar } from 'notistack';
|
|
import { Box, Stack, Typography } from '@mui/material';
|
|
import { AppButton, AppLoading, BankStatusPanel, EmptyState, ErrorState, RhfTextField } from '@/components';
|
|
import { useNurseBankAccounts, useAddNurseBankAccount, useSetPrimaryBankAccount } from '@/services/nurse';
|
|
import { isValidSheba } from '@/services/nurse/iban';
|
|
import { deriveBankStatus } from '@/services/nurse/types';
|
|
|
|
interface BankFormValues {
|
|
iban: string;
|
|
holder: string;
|
|
}
|
|
|
|
/**
|
|
* 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, isError, refetch } = useNurseBankAccounts();
|
|
const addAccount = useAddNurseBankAccount();
|
|
const setPrimary = useSetPrimaryBankAccount();
|
|
|
|
const [showForm, setShowForm] = useState(false);
|
|
const form = useForm<BankFormValues>({ mode: 'onTouched', defaultValues: { iban: '', holder: '' } });
|
|
const { handleSubmit, reset } = form;
|
|
|
|
const accounts = data ?? [];
|
|
const showFormNow = !isLoading && !isError && (accounts.length === 0 || showForm);
|
|
|
|
const submit = (values: BankFormValues) => {
|
|
addAccount.mutate(
|
|
{ iban: values.iban, accountHolderName: values.holder.trim() },
|
|
{
|
|
onSuccess: () => {
|
|
reset();
|
|
setShowForm(false);
|
|
enqueueSnackbar(t('added'), { variant: 'success' });
|
|
},
|
|
onError: () => enqueueSnackbar(t('add_error'), { variant: 'error' }),
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, maxWidth: 560 }}>
|
|
<Box>
|
|
<Typography variant="h5" component="h1">
|
|
{t('title')}
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
{t('subtitle')}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{isLoading ? <AppLoading /> : null}
|
|
|
|
{isError ? <ErrorState message={t('load_error')} retryLabel={tc('retry')} onRetry={() => refetch()} /> : null}
|
|
|
|
{!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 ? (
|
|
<FormProvider {...form}>
|
|
<Stack component="form" noValidate onSubmit={handleSubmit(submit)} sx={{ gap: 2 }}>
|
|
<RhfTextField<BankFormValues>
|
|
name="iban"
|
|
label={t('iban_label')}
|
|
helperText={t('iban_hint')}
|
|
transform={(raw) => raw.toUpperCase()}
|
|
rules={{ validate: (value) => isValidSheba(String(value ?? '')) || t('iban_invalid') }}
|
|
slotProps={{ htmlInput: { dir: 'ltr', style: { textAlign: 'start', letterSpacing: 1 } } }}
|
|
fullWidth
|
|
/>
|
|
<RhfTextField<BankFormValues>
|
|
name="holder"
|
|
label={t('holder_label')}
|
|
helperText={t('holder_hint')}
|
|
rules={{ validate: (value) => String(value ?? '').trim().length > 0 || t('holder_required') }}
|
|
fullWidth
|
|
/>
|
|
<Stack direction="row" sx={{ gap: 1 }}>
|
|
<AppButton type="submit" color="primary" variant="contained" startIcon="bank" disabled={addAccount.isPending}>
|
|
{addAccount.isPending ? t('submitting') : t('submit')}
|
|
</AppButton>
|
|
{accounts.length > 0 ? (
|
|
<AppButton
|
|
variant="text"
|
|
onClick={() => {
|
|
setShowForm(false);
|
|
reset();
|
|
}}
|
|
disabled={addAccount.isPending}
|
|
>
|
|
{tc('cancel')}
|
|
</AppButton>
|
|
) : null}
|
|
</Stack>
|
|
</Stack>
|
|
</FormProvider>
|
|
) : null}
|
|
</Box>
|
|
);
|
|
}
|