diff --git a/assets/admin/components/appointments/ConfirmAppointmentModal.test.tsx b/assets/admin/components/appointments/ConfirmAppointmentModal.test.tsx index 791ce62b..d98a49a9 100644 --- a/assets/admin/components/appointments/ConfirmAppointmentModal.test.tsx +++ b/assets/admin/components/appointments/ConfirmAppointmentModal.test.tsx @@ -392,3 +392,70 @@ describe('ConfirmAppointmentModal — ویزارد', () => { expect(screen.getByRole('button', { name: 'تأیید و قطعی کردن' })).toBeInTheDocument(); }); }); + +/** جمع نوبتِ تست: ۲۱۲۰۰۰۰ + ۸۸۰۰۰۰ ریال = ۳۰۰٬۰۰۰ تومان. */ +describe('ConfirmAppointmentModal — میان‌بُرهای درصدی', () => { + const percentButton = (percent: string) => + screen.getAllByRole('button', { name: `${percent}٪` })[0]; + + it('کلیک روی هر درصد، همان کسر از سهم بیمار را در مبلغ می‌گذارد', () => { + render(); + const [amount] = goToPayment(); + + fireEvent.click(percentButton('۲۰')); + expect(amount).toHaveValue('۶۰٬۰۰۰'); + + fireEvent.click(percentButton('۵۰')); + expect(amount).toHaveValue('۱۵۰٬۰۰۰'); + + fireEvent.click(percentButton('۷۰')); + expect(amount).toHaveValue('۲۱۰٬۰۰۰'); + + fireEvent.click(percentButton('۱۰۰')); + expect(amount).toHaveValue('۳۰۰٬۰۰۰'); + }); + + it('درصدِ اعمال‌شده تا مرحلهٔ ثبت می‌ماند و به سرور می‌رسد', async () => { + render(); + goToPayment(); + + fireEvent.click(percentButton('۵۰')); + goToReview(); + fireEvent.click(screen.getByRole('button', { name: 'تأیید و قطعی کردن' })); + + await waitFor(() => expect(post).toHaveBeenCalled()); + expect(post.mock.calls[0][1].payments).toEqual([ + { method: 'cash', amount_rials: 1_500_000 }, + ]); + }); + + it('هر ردیف درصد خودش را دارد؛ ردیف دیگر دست نمی‌خورد', () => { + render(); + goToPayment(); + + fireEvent.click(screen.getByRole('button', { name: /افزودن روش/ })); + const rows = amountInputs(); + expect(rows).toHaveLength(2); + + // ۲۰٪ روی ردیف دوم، بدون اثر روی ردیف اول که پیش‌فرضِ کامل دارد. + const secondRowPercent = screen.getAllByRole('button', { name: '۲۰٪' })[1]; + fireEvent.click(secondRowPercent); + + expect(rows[0]).toHaveValue('۳۰۰٬۰۰۰'); + expect(rows[1]).toHaveValue('۶۰٬۰۰۰'); + }); + + it('نوبتِ بدون هزینه اصلاً دکمهٔ درصدی ندارد', () => { + renderWithProviders( + {}} + />, + ); + nextStep(); + + expect(screen.queryByRole('button', { name: '۵۰٪' })).not.toBeInTheDocument(); + }); +}); diff --git a/assets/admin/components/appointments/ConfirmAppointmentModal.tsx b/assets/admin/components/appointments/ConfirmAppointmentModal.tsx index 3375d224..4cdb4130 100644 --- a/assets/admin/components/appointments/ConfirmAppointmentModal.tsx +++ b/assets/admin/components/appointments/ConfirmAppointmentModal.tsx @@ -5,7 +5,7 @@ import { UserCircleIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline import { api } from '../../lib/api'; import type { ApiResponse } from '../../lib/api'; import type { BankAccount, Pos } from '../../hooks/usePaymentMethods'; -import { formatRial, rialToToman, tomanToRial } from '../../lib/utils'; +import { formatNumber, formatRial, rialToToman, tomanToRial } from '../../lib/utils'; import { DEFAULT_SERVICE_CATEGORY } from '../../lib/insuranceShares'; import { useAppointmentInsurance } from '../../hooks/useAppointmentInsurance'; import Modal from '../ui/Modal'; @@ -13,6 +13,9 @@ import PriceInput from '../ui/PriceInput'; import SearchableSelect from '../ui/SearchableSelect'; import Stepper from '../ui/Stepper'; +/** میان‌بُرهای مبلغ روی هر ردیف پرداخت — درصدی از سهم بیمار. */ +const PAYMENT_PERCENTS = [20, 50, 70, 100]; + /** همان چهار روشِ SessionPayment::METHODS در بک‌اند. */ const METHOD_OPTIONS = [ { value: 'cash', label: 'پرداخت نقدی' }, @@ -254,6 +257,11 @@ export default function ConfirmAppointmentModal({ patchRow(id, { method, methodUuid: '', reference: '' }); } + /** درصدِ سهم بیمار روی همان ردیف می‌نشیند؛ ردیف‌های دیگر دست‌نخورده می‌مانند. */ + function applyPercent(id: number, percent: number) { + patchRow(id, { amountToman: rialToToman(Math.round((payable * percent) / 100)) }); + } + function addRow() { setTouched(true); // ردیفِ جدید پیش‌فرض با باقی‌مانده پر می‌شود تا تسویه سریع‌تر باشد. @@ -492,6 +500,39 @@ export default function ConfirmAppointmentModal({ )} + {/* میان‌بُرهای درصدی — پرداخت جزئی رایج است و تایپ دستیِ مبلغ خطا می‌آورد. */} + {payable > 0 && ( +
+ {PAYMENT_PERCENTS.map((percent) => { + const active = tomanToRial(r.amountToman) === Math.round((payable * percent) / 100); + return ( + + ); + })} + + از {formatRial(payable)} + +
+ )} + {/* جزئیاتِ کارت‌خوان: انتخاب دستگاهِ ثبت‌شده + شناسه تراکنش */} {r.method === 'pos' && (