Users typing on a Persian keyboard produced two distinct failures. Fields with type="number" silently returned an empty string — the browser rejects Persian digits, so the value was lost and saved as empty or zero. Text fields passed the Persian characters straight through to the database, where a mobile stored as ۰۹۱۲… never matches 09… again. The secretary form hit the second case with no validation at all. Frontend: - Adds digitsOnly() and the national-code schemas to lib/utils, plus lib/forms with numericField()/latinDigitsField() wrappers for React Hook Form fields. - Converts every type="number" input to type="text" inputMode="numeric" with digit normalization; none remain. Fields that legitimately carry non-digits (sheba, landline) only get the digits translated, keeping IR and separators. - Points the patient national-code and mobile schemas at the shared normalizing schemas, which accept Persian input instead of rejecting it. - Drops two duplicate local digit converters in favour of the shared helper. Backend: - Adds NumericFieldNormalizerSubscriber, translating digits in whitelisted numeric keys of JSON request bodies under /api/v1/ before controllers run, so nobat724_front and clinic-pro-tauri are covered too. Translation only — no characters are stripped, non-string values and other keys are untouched. Three component tests asserted on role="spinbutton" and numeric input values; both are properties of type="number", so they were updated to match the new text inputs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
import Modal from '../ui/Modal';
|
|
import SearchableSelect from '../ui/SearchableSelect';
|
|
import { BANK_OPTIONS } from './banks';
|
|
import {
|
|
useCreateBankAccount,
|
|
useUpdateBankAccount,
|
|
type BankAccount,
|
|
} from '../../hooks/usePaymentMethods';
|
|
import { digitsOnly, toEnglishDigits } from '../../lib/utils';
|
|
|
|
/**
|
|
* فرم افزودن/ویرایش حساب بانکی — پورت مبدأ ModalAddBankAccount.jsx.
|
|
* اگر `account` داده شود حالت ویرایش، وگرنه افزودن.
|
|
*/
|
|
export default function BankAccountFormModal({
|
|
open,
|
|
account,
|
|
onClose,
|
|
}: {
|
|
open: boolean;
|
|
account: BankAccount | null;
|
|
onClose: () => void;
|
|
}) {
|
|
const isEdit = !!account;
|
|
const [bankName, setBankName] = useState('');
|
|
const [cardNumber, setCardNumber] = useState('');
|
|
const [accountNumber, setAccountNumber] = useState('');
|
|
const [shabaNumber, setShabaNumber] = useState('');
|
|
|
|
const createMut = useCreateBankAccount();
|
|
const updateMut = useUpdateBankAccount();
|
|
const pending = createMut.isPending || updateMut.isPending;
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
setBankName(account?.bank_name ?? '');
|
|
setCardNumber(account?.card_number ?? '');
|
|
setAccountNumber(account?.account_number ?? '');
|
|
setShabaNumber(account?.shaba_number ?? '');
|
|
}, [open, account]);
|
|
|
|
const submit = () => {
|
|
if (!bankName.trim()) { toast.error('نام بانک الزامی است'); return; }
|
|
if (!accountNumber.trim()) { toast.error('شماره حساب الزامی است'); return; }
|
|
|
|
const body = {
|
|
bank_name: bankName.trim(),
|
|
account_number: accountNumber.trim(),
|
|
card_number: cardNumber.trim(),
|
|
shaba_number: shabaNumber.trim(),
|
|
};
|
|
|
|
const onSuccess = () => {
|
|
toast.success(isEdit ? 'حساب بانکی ویرایش شد' : 'حساب بانکی اضافه شد');
|
|
onClose();
|
|
};
|
|
const onError = (e: unknown) => toast.error(e instanceof Error ? e.message : 'خطا در ذخیره');
|
|
|
|
if (isEdit && account) {
|
|
updateMut.mutate({ uuid: account.uuid, body }, { onSuccess, onError });
|
|
} else {
|
|
createMut.mutate(body, { onSuccess, onError });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
open={open}
|
|
onClose={onClose}
|
|
title={isEdit ? 'ویرایش حساب بانکی' : 'افزودن حساب بانکی'}
|
|
size="sm"
|
|
footer={
|
|
<button className="btn primary" style={{ width: '100%' }} disabled={pending} onClick={submit}>
|
|
{pending ? '...' : isEdit ? 'ذخیره تغییرات' : 'اضافه کردن حساب بانکی'}
|
|
</button>
|
|
}
|
|
>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<div>
|
|
<label className="field-label">نام بانک</label>
|
|
<SearchableSelect
|
|
options={BANK_OPTIONS}
|
|
value={bankName || null}
|
|
onChange={(v) => setBankName(v != null ? String(v) : '')}
|
|
placeholder="انتخاب بانک"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="field-label">شماره کارت</label>
|
|
<input className="input" type="tel" inputMode="numeric" dir="ltr" value={cardNumber} onChange={(e) => setCardNumber(digitsOnly(e.target.value, 16))} placeholder="شماره کارت" />
|
|
</div>
|
|
<div>
|
|
<label className="field-label">شماره حساب</label>
|
|
<input className="input" type="tel" inputMode="numeric" dir="ltr" value={accountNumber} onChange={(e) => setAccountNumber(digitsOnly(e.target.value))} placeholder="شماره حساب" />
|
|
</div>
|
|
<div>
|
|
<label className="field-label">شبا</label>
|
|
<input className="input" inputMode="numeric" dir="ltr" value={shabaNumber} onChange={(e) => setShabaNumber(toEnglishDigits(e.target.value).toUpperCase())} placeholder="شبا" />
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|