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:
@@ -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,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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user