feat(appointment): add percentage shortcuts for patient share in payment rows

This commit is contained in:
hamed
2026-08-10 10:38:56 +03:30
parent a8699065b8
commit 505ab412a3
2 changed files with 109 additions and 1 deletions
@@ -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(
<ConfirmAppointmentModal
open
appointmentUuid="a2"
appointment={{ uuid: 'a2', version: 1, visit_price_rials: 0, service_items: [] }}
onClose={() => {}}
/>,
);
nextStep();
expect(screen.queryByRole('button', { name: '۵۰٪' })).not.toBeInTheDocument();
});
});
@@ -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({
)}
</div>
{/* میان‌بُرهای درصدی — پرداخت جزئی رایج است و تایپ دستیِ مبلغ خطا می‌آورد. */}
{payable > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
{PAYMENT_PERCENTS.map((percent) => {
const active = tomanToRial(r.amountToman) === Math.round((payable * percent) / 100);
return (
<button
key={percent}
type="button"
className="btn sm"
onClick={() => applyPercent(r.id, percent)}
title={formatRial(Math.round((payable * percent) / 100))}
style={{
height: 30,
padding: '0 12px',
fontSize: 12,
borderRadius: 'var(--r-pill)',
border: `1px solid ${active ? 'var(--primary)' : 'var(--border)'}`,
background: active ? 'var(--primary-soft)' : 'var(--surface)',
color: active ? 'var(--primary)' : 'var(--text-2)',
fontWeight: active ? 700 : 500,
}}
>
{formatNumber(percent)}٪
</button>
);
})}
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>
از {formatRial(payable)}
</span>
</div>
)}
{/* جزئیاتِ کارت‌خوان: انتخاب دستگاهِ ثبت‌شده + شناسه تراکنش */}
{r.method === 'pos' && (
<div style={{ display: 'flex', gap: 10 }}>