feat(appointment): add percentage shortcuts for patient share in payment rows
This commit is contained in:
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user