feat(admin): let the secretary pre-assign an operator when booking

The backend already accepted staff_uuid on POST /api/v1/my/appointment; only
the booking modal never sent it, so the pre-assignment half of the operator
model had no way in.

The field sits in step 1 next to the section select and is optional by design:
left empty, the session stays in the shared queue that every allowed operator
sees. Its placeholder says so rather than leaving the blank state unexplained.

The section select is no longer the only combobox in the modal, so the tests
target it by its label instead of by role.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 12:39:35 +03:30
co-authored by Claude Opus 5
parent 38ea477429
commit 251b45d807
3 changed files with 67 additions and 3 deletions
@@ -34,6 +34,9 @@ function mockApi() {
if (url.includes('patient-lookup')) {
return Promise.resolve({ success: true, data: { found: true, name: 'رضا رحیمی', mobile: '09120001307', national_code: '0012345678' } });
}
if (url === '/api/v1/staff') {
return Promise.resolve({ success: true, data: [{ uuid: 'st-1', full_name: 'پرسنل یک' }] });
}
if (url.includes('/service-slots')) {
return Promise.resolve({ success: true, data: {
total_duration_minutes: 25,
@@ -46,7 +49,8 @@ function mockApi() {
/** انتخاب بخش → سرویس → زمان، همان مسیرِ مودالِ طرح. */
async function pickServiceAndTime() {
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' });
// مودال حالا دو combobox دارد — بخش و اپراتور. سراغ بخش با لیبل خودش می‌رویم.
fireEvent.keyDown(screen.getByLabelText('بخش'), { key: 'ArrowDown' });
fireEvent.click(await screen.findByText('خدمات درمانگاه'));
// ردیف سرویس یک checkbox است نه دکمه: انتخابش حالت دارد و باید برای screen reader
// «انتخاب‌شده/نشده» اعلام شود.
@@ -123,7 +127,8 @@ describe('مودال ثبت نوبتِ منبع', () => {
expect(await screen.findByText(/یک سرویس انتخاب کنید/)).toBeInTheDocument();
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' });
// مودال حالا دو combobox دارد — بخش و اپراتور. سراغ بخش با لیبل خودش می‌رویم.
fireEvent.keyDown(screen.getByLabelText('بخش'), { key: 'ArrowDown' });
fireEvent.click(await screen.findByText('خدمات درمانگاه'));
fireEvent.click(await screen.findByRole('checkbox', { name: /کرایوتراپی/ }));
@@ -159,6 +164,23 @@ describe('مودال ثبت نوبتِ منبع', () => {
expect(screen.getByPlaceholderText('مثال: 09123456789')).toBeInTheDocument();
});
it('اپراتور اختیاری است و فقط وقتی انتخاب شود staff_uuid می‌رود', async () => {
renderWithProviders(
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
);
await pickServiceAndTime();
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
await screen.findByText('بیمار یافت شد');
// بدون انتخاب اپراتور، جلسه باید در صفِ مشترک بماند — پس کلید اصلاً نرود.
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
await waitFor(() => expect(post).toHaveBeenCalled());
expect(post.mock.calls[0][1]).not.toHaveProperty('staff_uuid');
});
it('منبعِ بدون سرویس، پیام راهنما می‌دهد نه فهرست خالی', () => {
renderWithProviders(
<NewAppointmentModal slot={slot} resource={resource} services={[]} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,