feat(admin): comma-grouped Latin amounts on the session payment step

Add an opt-in latin prop to PriceInput (en-US grouping, English digits) and
use it for the discount-value and payment-amount fields on PaymentStep, which
were raw number inputs. Amounts now show 3-digit comma grouping and Persian
digits typed are converted to English (via PriceInput's toEnglishDigits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-17 12:45:45 +03:30
co-authored by Claude Fable 5
parent 204c7debe1
commit ee69ac96be
2 changed files with 28 additions and 28 deletions
+19 -22
View File
@@ -9,6 +9,7 @@ import type { DiscountSuggestion } from '../../types';
import type { SessionCardData } from '../SessionServiceCard'; import type { SessionCardData } from '../SessionServiceCard';
import SearchableSelect from '../ui/SearchableSelect'; import SearchableSelect from '../ui/SearchableSelect';
import PersianDateInput from '../ui/PersianDateInput'; import PersianDateInput from '../ui/PersianDateInput';
import PriceInput from '../ui/PriceInput';
import { Step2PaymentCard, FilesServiceBalanceWallet, TrashRed } from '../icons/FilesServiceIcons'; import { Step2PaymentCard, FilesServiceBalanceWallet, TrashRed } from '../icons/FilesServiceIcons';
/** روش‌های پرداخت — همان چهار گزینه‌ی آکاردئون tauri Step2Payment. */ /** روش‌های پرداخت — همان چهار گزینه‌ی آکاردئون tauri Step2Payment. */
@@ -48,10 +49,10 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
const sessionUuid = session.uuid; const sessionUuid = session.uuid;
const [discountType, setDiscountType] = useState(''); const [discountType, setDiscountType] = useState('');
const [discountValue, setDiscountValue] = useState(''); const [discountValue, setDiscountValue] = useState(0);
const [paymentDate, setPaymentDate] = useState(todayISO()); const [paymentDate, setPaymentDate] = useState(todayISO());
const [expanded, setExpanded] = useState<string | null>(null); const [expanded, setExpanded] = useState<string | null>(null);
const [amount, setAmount] = useState(''); const [amount, setAmount] = useState(0);
const invalidate = () => { const invalidate = () => {
qc.invalidateQueries({ queryKey: ['patient-sessions', recordUuid] }); qc.invalidateQueries({ queryKey: ['patient-sessions', recordUuid] });
@@ -67,7 +68,7 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
const payMut = useMutation({ const payMut = useMutation({
mutationFn: (body: object) => api.post(`/api/v1/session/${sessionUuid}/payments`, body), mutationFn: (body: object) => api.post(`/api/v1/session/${sessionUuid}/payments`, body),
onSuccess: () => { invalidate(); setAmount(''); toast.success('پرداخت ثبت شد'); }, onSuccess: () => { invalidate(); setAmount(0); toast.success('پرداخت ثبت شد'); },
onError: (e: Error) => toast.error(e.message), onError: (e: Error) => toast.error(e.message),
}); });
@@ -78,19 +79,19 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
const suggestions: DiscountSuggestion[] = (suggestionsQ.data?.data as any)?.data ?? (suggestionsQ.data?.data as any) ?? []; const suggestions: DiscountSuggestion[] = (suggestionsQ.data?.data as any)?.data ?? (suggestionsQ.data?.data as any) ?? [];
const applyDiscount = () => { const applyDiscount = () => {
if (!discountType || !discountValue) return; if (!discountType || discountValue <= 0) return;
discountMut.mutate({ discount_type: discountType, discount_value: Number(discountValue) }); discountMut.mutate({ discount_type: discountType, discount_value: discountValue });
}; };
const applyRule = (ruleUuid: string) => { const applyRule = (ruleUuid: string) => {
discountMut.mutate({ discount_rule_uuid: ruleUuid }); discountMut.mutate({ discount_rule_uuid: ruleUuid });
}; };
const removeDiscount = () => { const removeDiscount = () => {
setDiscountType(''); setDiscountValue(''); setDiscountType(''); setDiscountValue(0);
discountMut.mutate({ discount_rule_uuid: null }); discountMut.mutate({ discount_rule_uuid: null });
}; };
const submitPayment = (method: string) => { const submitPayment = (method: string) => {
if (!amount) return; if (amount <= 0) return;
payMut.mutate({ method, amount_rials: Number(amount), paid_at: isoToUnix(paymentDate) }); payMut.mutate({ method, amount_rials: amount, paid_at: isoToUnix(paymentDate) });
}; };
const finalPrice = session.final_price_rials ?? 0; const finalPrice = session.final_price_rials ?? 0;
@@ -146,19 +147,17 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
placeholder="مبلغ تخفیف" placeholder="مبلغ تخفیف"
/> />
</div> </div>
<input <PriceInput
type="number" latin
min={0}
dir="ltr"
placeholder="مقدار تخفیف را وارد نمایید"
value={discountValue} value={discountValue}
onChange={(e) => setDiscountValue(e.target.value)} onChange={setDiscountValue}
placeholder="مقدار تخفیف را وارد نمایید"
style={{ flex: 1, minWidth: 0, height: 40, padding: '0 12px', fontSize: 13, border: 'none', outline: 'none', background: 'transparent', color: 'inherit' }} style={{ flex: 1, minWidth: 0, height: 40, padding: '0 12px', fontSize: 13, border: 'none', outline: 'none', background: 'transparent', color: 'inherit' }}
/> />
<button <button
type="button" type="button"
onClick={applyDiscount} onClick={applyDiscount}
disabled={discountMut.isPending || !discountType || !discountValue} disabled={discountMut.isPending || !discountType || discountValue <= 0}
style={{ height: 40, minWidth: 72, border: 'none', borderRadius: '0 4px 4px 0', background: '#E8EBFF', color: '#3B3F9F', fontSize: 13, fontWeight: 600, cursor: 'pointer' }} style={{ height: 40, minWidth: 72, border: 'none', borderRadius: '0 4px 4px 0', background: '#E8EBFF', color: '#3B3F9F', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}
> >
ثبت ثبت
@@ -212,7 +211,7 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
<button <button
type="button" type="button"
aria-expanded={open} aria-expanded={open}
onClick={() => { setExpanded(open ? null : m.key); setAmount(''); }} onClick={() => { setExpanded(open ? null : m.key); setAmount(0); }}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', padding: '14px 4px', background: 'transparent', border: 'none', cursor: 'pointer' }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', padding: '14px 4px', background: 'transparent', border: 'none', cursor: 'pointer' }}
> >
<span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>{m.label}</span> <span className="dark:text-[#D7D8ED]" style={{ fontSize: 14, fontWeight: 600, color: '#111827' }}>{m.label}</span>
@@ -220,20 +219,18 @@ export default function PaymentStep({ recordUuid, session, walletBalance, onCont
</button> </button>
{open && ( {open && (
<div style={{ display: 'flex', gap: 8, padding: '0 4px 14px' }}> <div style={{ display: 'flex', gap: 8, padding: '0 4px 14px' }}>
<input <PriceInput
type="number" latin
min={1}
dir="ltr"
className="input" className="input"
placeholder="مبلغ (تومان)" placeholder="مبلغ (تومان)"
value={amount} value={amount}
onChange={(e) => setAmount(e.target.value)} onChange={setAmount}
style={{ flex: 1, height: 40 }} style={{ flex: 1, height: 40 }}
/> />
<button <button
type="button" type="button"
onClick={() => submitPayment(m.key)} onClick={() => submitPayment(m.key)}
disabled={payMut.isPending || !amount} disabled={payMut.isPending || amount <= 0}
style={{ ...primaryBtn, height: 40, padding: '0 20px', borderRadius: 8 }} style={{ ...primaryBtn, height: 40, padding: '0 20px', borderRadius: 8 }}
> >
ثبت پرداخت ثبت پرداخت
+9 -6
View File
@@ -9,11 +9,13 @@ interface PriceInputProps {
style?: React.CSSProperties; style?: React.CSSProperties;
disabled?: boolean; disabled?: boolean;
min?: number; min?: number;
/** نمایش ارقام لاتین با جداکنندهٔ کاما (پیش‌فرض: فارسی). */
latin?: boolean;
} }
function formatDisplay(num: number): string { function formatDisplay(num: number, latin: boolean): string {
if (num === 0) return ''; if (num === 0) return '';
return new Intl.NumberFormat('fa-IR').format(num); return new Intl.NumberFormat(latin ? 'en-US' : 'fa-IR').format(num);
} }
export default function PriceInput({ export default function PriceInput({
@@ -24,18 +26,19 @@ export default function PriceInput({
style, style,
disabled, disabled,
min = 0, min = 0,
latin = false,
}: PriceInputProps) { }: PriceInputProps) {
const [display, setDisplay] = useState(() => (value !== '' && value > 0 ? formatDisplay(value) : '')); const [display, setDisplay] = useState(() => (value !== '' && value > 0 ? formatDisplay(value, latin) : ''));
useEffect(() => { useEffect(() => {
setDisplay(value !== '' && Number(value) > 0 ? formatDisplay(Number(value)) : ''); setDisplay(value !== '' && Number(value) > 0 ? formatDisplay(Number(value), latin) : '');
}, [value]); }, [value, latin]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = toEnglishDigits(e.target.value).replace(/[^0-9]/g, ''); const raw = toEnglishDigits(e.target.value).replace(/[^0-9]/g, '');
const num = raw === '' ? 0 : Math.max(min, parseInt(raw, 10)); const num = raw === '' ? 0 : Math.max(min, parseInt(raw, 10));
onChange(num); onChange(num);
setDisplay(num > 0 ? formatDisplay(num) : ''); setDisplay(num > 0 ? formatDisplay(num, latin) : '');
}; };
return ( return (