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();
|
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 { api } from '../../lib/api';
|
||||||
import type { ApiResponse } from '../../lib/api';
|
import type { ApiResponse } from '../../lib/api';
|
||||||
import type { BankAccount, Pos } from '../../hooks/usePaymentMethods';
|
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 { DEFAULT_SERVICE_CATEGORY } from '../../lib/insuranceShares';
|
||||||
import { useAppointmentInsurance } from '../../hooks/useAppointmentInsurance';
|
import { useAppointmentInsurance } from '../../hooks/useAppointmentInsurance';
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
@@ -13,6 +13,9 @@ import PriceInput from '../ui/PriceInput';
|
|||||||
import SearchableSelect from '../ui/SearchableSelect';
|
import SearchableSelect from '../ui/SearchableSelect';
|
||||||
import Stepper from '../ui/Stepper';
|
import Stepper from '../ui/Stepper';
|
||||||
|
|
||||||
|
/** میانبُرهای مبلغ روی هر ردیف پرداخت — درصدی از سهم بیمار. */
|
||||||
|
const PAYMENT_PERCENTS = [20, 50, 70, 100];
|
||||||
|
|
||||||
/** همان چهار روشِ SessionPayment::METHODS در بکاند. */
|
/** همان چهار روشِ SessionPayment::METHODS در بکاند. */
|
||||||
const METHOD_OPTIONS = [
|
const METHOD_OPTIONS = [
|
||||||
{ value: 'cash', label: 'پرداخت نقدی' },
|
{ value: 'cash', label: 'پرداخت نقدی' },
|
||||||
@@ -254,6 +257,11 @@ export default function ConfirmAppointmentModal({
|
|||||||
patchRow(id, { method, methodUuid: '', reference: '' });
|
patchRow(id, { method, methodUuid: '', reference: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** درصدِ سهم بیمار روی همان ردیف مینشیند؛ ردیفهای دیگر دستنخورده میمانند. */
|
||||||
|
function applyPercent(id: number, percent: number) {
|
||||||
|
patchRow(id, { amountToman: rialToToman(Math.round((payable * percent) / 100)) });
|
||||||
|
}
|
||||||
|
|
||||||
function addRow() {
|
function addRow() {
|
||||||
setTouched(true);
|
setTouched(true);
|
||||||
// ردیفِ جدید پیشفرض با باقیمانده پر میشود تا تسویه سریعتر باشد.
|
// ردیفِ جدید پیشفرض با باقیمانده پر میشود تا تسویه سریعتر باشد.
|
||||||
@@ -492,6 +500,39 @@ export default function ConfirmAppointmentModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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' && (
|
{r.method === 'pos' && (
|
||||||
<div style={{ display: 'flex', gap: 10 }}>
|
<div style={{ display: 'flex', gap: 10 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user