feat: section-then-service picker with cross-section accumulation

Rework slot-mode service selection: pick a section, its services show as a
checkbox list, and chosen services collect into a removable 'selected'
chip list that persists when switching to another section (services from
multiple sections accumulate). Changing the section no longer clears the
selection. Add a test covering multi-section accumulation and chip removal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-16 10:18:10 +03:30
co-authored by Claude Opus 4.8
parent 08aa3a78b8
commit 029b5f632e
2 changed files with 117 additions and 38 deletions
@@ -108,6 +108,53 @@ describe('AppointmentCreatePage — افزودن نوبت', () => {
expect(body).toMatchObject({ patient_name: 'حامد حسینی', patient_mobile: '09210671745', patient_national_code: '0012345675' });
});
it('slot mode: accumulates services across multiple sections and removes them (happy path)', async () => {
get.mockImplementation((url: string) => {
if (url === '/api/v1/service-sections')
return Promise.resolve({ success: true, data: [{ uuid: 'sec1', name: 'زیبایی' }, { uuid: 'sec2', name: 'پوست' }] });
if (url === '/api/v1/service-items/sec1')
return Promise.resolve({ success: true, data: [{ uuid: 'i1', name: 'لیزر' }, { uuid: 'i2', name: 'ماساژ' }] });
if (url === '/api/v1/service-items/sec2')
return Promise.resolve({ success: true, data: [{ uuid: 'i3', name: 'پاکسازی' }] });
return Promise.resolve({ success: true, data: [] });
});
const pickSection = async (optionLabel: string) => {
const input = document.getElementById('appt-section-select') as HTMLInputElement;
fireEvent.focus(input);
fireEvent.keyDown(input, { key: 'ArrowDown' });
fireEvent.click(await screen.findByText(optionLabel));
};
renderWithProviders(<AppointmentCreatePage />, { route: '/admin/appointments/new' });
fireEvent.click(screen.getByText('مراجعه کننده جدید'));
fireEvent.change(screen.getByPlaceholderText('نام و نام خانوادگی مراجعه کننده'), { target: { value: 'علی محمدی' } });
fireEvent.change(screen.getByPlaceholderText('شماره تماس مراجعه کننده'), { target: { value: '09121234567' } });
fireEvent.change(screen.getByPlaceholderText('کد ملی مراجعه کننده'), { target: { value: '1234567891' } });
// بخش اول → دو سرویس انتخاب
await pickSection('زیبایی');
fireEvent.click(await screen.findByText('لیزر'));
fireEvent.click(await screen.findByText('ماساژ'));
// بخش دوم → یک سرویس؛ لیستِ انباشته باید سرویس‌های بخش قبل را نگه دارد
await pickSection('پوست');
fireEvent.click(await screen.findByText('پاکسازی'));
expect(screen.getByText('سرویس‌های انتخاب‌شده (3)')).toBeInTheDocument();
// حذف یک سرویس از لیست
fireEvent.click(screen.getByLabelText('حذف ماساژ'));
expect(screen.getByText('سرویس‌های انتخاب‌شده (2)')).toBeInTheDocument();
fireEvent.click(screen.getByText('ثبت اطلاعات'));
await waitFor(() => expect(post).toHaveBeenCalled());
const [, body] = post.mock.calls[0];
expect(body.service_item_uuids).toEqual(['i1', 'i3']);
expect(body.duration_from_services).toBeUndefined(); // حالت اسلاتی: ساعت پایانِ دستی حفظ
});
it('picking an existing patient WITHOUT a national code keeps submit disabled until one is entered (boundary)', async () => {
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/patients?'))