feat: update session payment logic to ensure accurate payable amounts and reflect consumables in cost breakdown

- Adjusted the calculation of payable amounts in PaymentStep to align with server logic, ensuring overpayments are handled correctly.
- Enhanced DetailsStep to include consumables in the itemized cost breakdown, ensuring consistency with patient share calculations.
- Updated tests for SessionPaymentPage to validate new behavior regarding overpayments and consumable listings.
- Modified PatientController to register SessionPayment correctly when settling sessions via wallet, preventing double charges.
- Refactored WalletService to remove outdated methods and ensure wallet transactions reflect the correct amounts after discounts.
- Improved accessibility in SearchableSelect component by adding aria labels and ensuring proper role attributes for screen readers.
- Updated styles to ensure minimum touch targets meet WCAG guidelines for mobile usability.
This commit is contained in:
hamed
2026-07-19 13:29:46 +03:30
parent 0d9784b03a
commit 773f9d4d16
22 changed files with 251 additions and 70 deletions
@@ -24,7 +24,8 @@ describe('PersianDateInput', () => {
const onChange = vi.fn();
const { container } = render(<PersianDateInput value="2024-03-25" onChange={onChange} />);
fireEvent.click((container.firstChild as HTMLElement).firstChild as Element);
const target = Array.from(container.querySelectorAll('button')).find(
// تقویم با createPortal به document.body می‌رود، پس بیرون از container است
const target = Array.from(document.body.querySelectorAll('button')).find(
(b) => b.textContent?.trim() === '۱۵',
);
expect(target).toBeTruthy();
@@ -31,3 +31,25 @@ describe('SearchableSelect — تطبیق مقدار', () => {
expect(screen.queryByText('تامین اجتماعی')).not.toBeInTheDocument();
});
});
describe('SearchableSelect — نام دسترس‌پذیر', () => {
it('در نبود ariaLabel، از placeholder به‌عنوان نام استفاده می‌کند', () => {
render(<SearchableSelect options={insuranceOpts} onChange={() => {}} placeholder="نوع بیمه" />);
expect(screen.getByRole('combobox', { name: 'نوع بیمه' })).toBeInTheDocument();
});
it('ariaLabel بر placeholder اولویت دارد', () => {
render(<SearchableSelect options={insuranceOpts} onChange={() => {}} placeholder="انتخاب کنید..." ariaLabel="بیمه پایه" />);
expect(screen.getByRole('combobox', { name: 'بیمه پایه' })).toBeInTheDocument();
});
it('ariaLabelledBy به label قابل‌مشاهده وصل می‌شود', () => {
render(
<>
<span id="lbl-city">شهر</span>
<SearchableSelect options={[{ value: 9, label: 'یزد' }]} onChange={() => {}} ariaLabelledBy="lbl-city" />
</>,
);
expect(screen.getByRole('combobox', { name: 'شهر' })).toBeInTheDocument();
});
});
@@ -18,6 +18,14 @@ interface Props {
noOptionsMessage?: string;
inputId?: string;
height?: number;
/**
* نام دسترس‌پذیر فیلد. react-select ورودی داخلی خودش را بدون label رندر می‌کند و
* placeholder را هم به‌صورت div می‌گذارد نه attribute، پس بدون این، فیلد برای
* screen reader بی‌نام می‌ماند. پیش‌فرض روی placeholder می‌افتد.
*/
ariaLabel?: string;
/** اگر label قابل‌مشاهده‌ای وجود دارد، id آن را بده (بر ariaLabel اولویت دارد). */
ariaLabelledBy?: string;
}
export default function SearchableSelect({
@@ -31,6 +39,8 @@ export default function SearchableSelect({
noOptionsMessage = 'موردی یافت نشد',
inputId,
height = 42,
ariaLabel,
ariaLabelledBy,
}: Props) {
const darkMode = useUiStore((s) => s.darkMode);
@@ -118,6 +128,8 @@ export default function SearchableSelect({
noOptionsMessage={() => noOptionsMessage}
loadingMessage={() => 'در حال بارگذاری...'}
inputId={inputId}
aria-labelledby={ariaLabelledBy}
aria-label={ariaLabelledBy ? undefined : (ariaLabel ?? placeholder)}
/>
);
}