refactor(admin): restructure the new-appointment modal around its real steps

The booking modal presented one flat scroll of fields whose order did not
match the order of the decisions behind them, and gave no reason when the
submit button stayed grey.

- Group the form into numbered steps (service+time, patient) so the order of
  decisions is visible. The optional visit-price collapse stays unnumbered —
  numbering an optional step reads as required.
- Show the first blocking condition above the footer instead of leaving a
  disabled button unexplained.
- Label the header chip's facts ("device:", "supervising doctor:") and add the
  appointment's Jalali date, which the modal never displayed at all.
- Replace the hand-rolled primary/ghost button pair with the design system's
  `.seg` + `.on`, and announce state via aria-pressed.
- Move autoFocus off the patient search in picker mode; the first decision is
  the section select above it.
- Give every input an id and its label an htmlFor.
- Surface a distinct error state for the slot query. A failed request used to
  fall through to "not enough free time", which sent users to another day for
  no reason.
- Raise the service remove button (18px), the duration pill and the time chips
  to at least the 32px hit target; mark service rows role=checkbox.
- Modal close button gets an accessible name; `.field` controls stretch to the
  full 40px box so the whole frame is clickable.

Runtime probe on the open modal goes from 4 unnamed icon controls, 1 unlabelled
field and 2 sub-32px controls to clean, across light/dark/compact/mobile.

The redesign-page probe now names the offending elements instead of only
counting them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 12:08:25 +03:30
co-authored by Claude Opus 5
parent 1f4f7e6927
commit 103d913cd0
6 changed files with 266 additions and 90 deletions
@@ -48,7 +48,9 @@ function mockApi() {
async function pickServiceAndTime() {
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' });
fireEvent.click(await screen.findByText('خدمات درمانگاه'));
fireEvent.click(await screen.findByRole('button', { name: /کرایوتراپی/ }));
// ردیف سرویس یک checkbox است نه دکمه: انتخابش حالت دارد و باید برای screen reader
// «انتخاب‌شده/نشده» اعلام شود.
fireEvent.click(await screen.findByRole('checkbox', { name: /کرایوتراپی/ }));
fireEvent.click(await screen.findByRole('button', { name: '12:00' }));
}
@@ -114,6 +116,49 @@ describe('مودال ثبت نوبتِ منبع', () => {
expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled();
});
it('دلیلِ غیرفعال بودنِ ثبت را می‌گوید و با پیشرفتِ فرم عوض می‌شود', async () => {
renderWithProviders(
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
);
expect(await screen.findByText(/یک سرویس انتخاب کنید/)).toBeInTheDocument();
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' });
fireEvent.click(await screen.findByText('خدمات درمانگاه'));
fireEvent.click(await screen.findByRole('checkbox', { name: /کرایوتراپی/ }));
// سرویس هست، زمان نه — پیام باید مرحلهٔ بعد را نشان دهد نه همان قبلی.
expect(await screen.findByText(/ساعت شروع را انتخاب کنید/)).toBeInTheDocument();
fireEvent.click(await screen.findByRole('button', { name: '12:00' }));
expect(await screen.findByText(/ابتدا بیمار را جستجو کنید/)).toBeInTheDocument();
});
it('تاریخ نوبت به‌صورت جلالی بالای فرم می‌آید', () => {
renderWithProviders(
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
);
// بدون این، کاربر روزِ در حال رزرو را هیچ‌جای مودال نمی‌دید.
expect(screen.getByText('تاریخ:')).toBeInTheDocument();
expect(screen.getByText('۱۴۰۵/۰۵/۱۳')).toBeInTheDocument();
});
it('معیار جستجو یک seg با حالتِ اعلام‌شده است', () => {
renderWithProviders(
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
);
const national = screen.getByRole('button', { name: 'کد ملی' });
const mobile = screen.getByRole('button', { name: 'شماره موبایل' });
expect(national).toHaveAttribute('aria-pressed', 'true');
expect(mobile).toHaveAttribute('aria-pressed', 'false');
fireEvent.click(mobile);
expect(screen.getByRole('button', { name: 'شماره موبایل' })).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByPlaceholderText('مثال: 09123456789')).toBeInTheDocument();
});
it('منبعِ بدون سرویس، پیام راهنما می‌دهد نه فهرست خالی', () => {
renderWithProviders(
<NewAppointmentModal slot={slot} resource={resource} services={[]} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,