From c4099f0f6345221ae2317d44103e7faabfcfd211 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Fri, 7 Aug 2026 16:28:13 +0330 Subject: [PATCH] =?UTF-8?q?feat(admin):=20default=20the=20booking=20staff?= =?UTF-8?q?=20from=20the=20service's=20protocol,=20and=20call=20it=20?= =?UTF-8?q?=D9=BE=D8=B1=D8=B3=D9=86=D9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The staff field started empty even when the service's treatment protocol already named who may perform it — a decision made once in the service settings and then asked again on every booking. The modal now reads that protocol and preselects its first staff member, but only until the user touches the field; otherwise a manual choice would be wiped on the next service change. A protocol with no staff leaves it empty and does not block submission. Renames the four user-facing 'اپراتور' strings to 'پرسنل', matching the record in /admin/staff that they all refer to. ResourcesPage keeps the word: there it names a kind of bookable resource (doctor, operator, room, device), not a ClinicStaff row. Drops a test whose premise the default invalidated; the two new ones cover both sides — protocol with staff sends staff_uuid, protocol without staff sends none. Co-Authored-By: Claude Opus 5 --- .../components/TreatmentCaseEditModal.tsx | 2 +- .../appointments/NewAppointmentModal.tsx | 31 ++++++++++++-- .../ResourceBookingModal.test.tsx | 40 ++++++++++++++++++- assets/admin/pages/StaffSessionDetailPage.tsx | 2 +- .../admin/pages/TreatmentCasesPage.test.tsx | 4 +- assets/admin/pages/TreatmentCasesPage.tsx | 2 +- 6 files changed, 71 insertions(+), 10 deletions(-) diff --git a/assets/admin/components/TreatmentCaseEditModal.tsx b/assets/admin/components/TreatmentCaseEditModal.tsx index 86d065a2..83330204 100644 --- a/assets/admin/components/TreatmentCaseEditModal.tsx +++ b/assets/admin/components/TreatmentCaseEditModal.tsx @@ -209,7 +209,7 @@ function EditForm({ detail, doctors, doctorsLoading, staff, staffLoading, onSave
- + {/* «در حال خواندن» با «تعریف نشده» یکی نیست؛ بدون این تفکیک، فهرستِ هنوز نیامده به‌صورت «پرسنلی ندارید» خوانده می‌شد. */} {staffLoading ? ( diff --git a/assets/admin/components/appointments/NewAppointmentModal.tsx b/assets/admin/components/appointments/NewAppointmentModal.tsx index e3afe56c..0c9bd041 100644 --- a/assets/admin/components/appointments/NewAppointmentModal.tsx +++ b/assets/admin/components/appointments/NewAppointmentModal.tsx @@ -68,6 +68,9 @@ export default function NewAppointmentModal({ // اپراتور اختیاری است: خالی گذاشتنش جلسه را در صفِ مشترکِ پرسنلِ مجاز می‌گذارد، // پر کردنش آن را از قبل به یک نفر می‌دهد. const [staffUuid, setStaffUuid] = useState(''); + // پیش‌فرضِ پروتکل فقط تا وقتی اعمال می‌شود که کاربر دست نزده باشد؛ وگرنه انتخاب + // دستیِ منشی با هر تغییرِ سرویس پاک می‌شد. + const [staffTouched, setStaffTouched] = useState(false); const role = useAuthStore(s => s.primaryRole); // نوبتِ منبع فقط مسیر پنل را دارد؛ اندپوینت ادمین `resource_uuid` نمی‌شناسد. @@ -99,6 +102,28 @@ export default function NewAppointmentModal({ staleTime: 60_000, }); + /** + * پرسنلِ مجازِ «طول درمان» همان سرویس. + * + * منشی نباید چیزی را دوباره انتخاب کند که در تنظیمات سرویس یک بار تعریف شده؛ + * پیش‌فرض همان است و تغییرش آزاد. + */ + const primaryService = pick.serviceUuids[0] ?? ''; + const protocolQ = useQuery>({ + queryKey: ['service-protocol', primaryService], + queryFn: () => api.get(`/api/v1/service-item/${primaryService}/treatment-protocol`), + enabled: primaryService !== '', + staleTime: 60_000, + }); + + const protocolStaff = protocolQ.data?.data?.staff ?? []; + + useEffect(() => { + if (staffTouched || protocolStaff.length === 0) return; + + setStaffUuid(protocolStaff[0].uuid); + }, [protocolStaff, staffTouched]); + const selfPricingQ = useQuery({ queryKey: ['insurance-pricing'], queryFn: () => api.get('/api/v1/insurance-pricing'), @@ -288,13 +313,13 @@ export default function NewAppointmentModal({ {/* اختیاری به‌عمد: پیش‌فرضِ سیستم صفِ مشترک است و منشی فقط وقتی دخالت می‌کند که بیمار اپراتور مشخصی خواسته باشد. */}
- + ({ value: s.uuid, label: s.full_name }))} value={staffUuid || null} - onChange={v => setStaffUuid(v ? String(v) : '')} - placeholder="در صف مشترک پرسنل بماند" + onChange={v => { setStaffUuid(v ? String(v) : ''); setStaffTouched(true); }} + placeholder={protocolQ.isLoading ? 'در حال خواندن پرسنل سرویس…' : 'در صف مشترک پرسنل بماند'} isLoading={staffQ.isLoading} isClearable height={40} diff --git a/assets/admin/components/appointments/ResourceBookingModal.test.tsx b/assets/admin/components/appointments/ResourceBookingModal.test.tsx index 01141568..4374abff 100644 --- a/assets/admin/components/appointments/ResourceBookingModal.test.tsx +++ b/assets/admin/components/appointments/ResourceBookingModal.test.tsx @@ -37,6 +37,9 @@ function mockApi() { if (url === '/api/v1/staff') { return Promise.resolve({ success: true, data: [{ uuid: 'st-1', full_name: 'پرسنل یک' }] }); } + if (url.includes('/treatment-protocol')) { + return Promise.resolve({ success: true, data: { staff: [{ uuid: 'st-1', name: 'پرسنل یک' }] } }); + } if (url.includes('/service-slots')) { return Promise.resolve({ success: true, data: { total_duration_minutes: 25, @@ -164,7 +167,41 @@ describe('مودال ثبت نوبتِ منبع', () => { expect(screen.getByPlaceholderText('مثال: 09123456789')).toBeInTheDocument(); }); - it('اپراتور اختیاری است و فقط وقتی انتخاب شود staff_uuid می‌رود', async () => { + /** آنچه در «طول درمان» سرویس تعریف شده نباید دوباره از منشی پرسیده شود. */ + it('پرسنلِ پروتکل سرویس را پیش‌فرض می‌فرستد', async () => { + renderWithProviders( + {}} onSuccess={() => {}} />, + ); + + await pickServiceAndTime(); + + fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } }); + fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); + await screen.findByText('بیمار یافت شد'); + + await waitFor(() => expect(screen.getByLabelText(/پرسنل/)).toBeInTheDocument()); + + fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' })); + await waitFor(() => expect(post).toHaveBeenCalled()); + expect(post.mock.calls[0][1]).toMatchObject({ staff_uuid: 'st-1' }); + }); + + /** پروتکل بدون پرسنل نباید فرم را قفل کند. */ + it('پروتکل بدون پرسنل، فیلد را خالی می‌گذارد و ثبت همچنان ممکن است', async () => { + get.mockImplementation((url: string) => { + if (url.includes('/treatment-protocol')) return Promise.resolve({ success: true, data: null }); + if (url.includes('patient-lookup')) { + return Promise.resolve({ success: true, data: { found: true, name: 'رضا رحیمی', mobile: '09120001307', national_code: '0012345678' } }); + } + if (url.includes('/service-slots')) { + return Promise.resolve({ success: true, data: { + total_duration_minutes: 25, + start_times: [{ start: START, end: START + 1500, start_time: '12:00', end_time: '12:25' }], + } }); + } + return Promise.resolve({ success: true, data: [] }); + }); + renderWithProviders( {}} onSuccess={() => {}} />, ); @@ -175,7 +212,6 @@ describe('مودال ثبت نوبتِ منبع', () => { 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'); diff --git a/assets/admin/pages/StaffSessionDetailPage.tsx b/assets/admin/pages/StaffSessionDetailPage.tsx index 46af7000..c313cf98 100644 --- a/assets/admin/pages/StaffSessionDetailPage.tsx +++ b/assets/admin/pages/StaffSessionDetailPage.tsx @@ -113,7 +113,7 @@ export default function StaffSessionDetailPage() {
{session.appointment && تاریخ: {formatDate(session.appointment.slot_start)}} - {session.performed_by && اپراتور: {session.performed_by.name}} + {session.performed_by && پرسنل: {session.performed_by.name}} {formatNumber(settled)} از {formatNumber(areas.length)} ناحیه انجام شده {elapsed && ( diff --git a/assets/admin/pages/TreatmentCasesPage.test.tsx b/assets/admin/pages/TreatmentCasesPage.test.tsx index c5765f71..4404fdd3 100644 --- a/assets/admin/pages/TreatmentCasesPage.test.tsx +++ b/assets/admin/pages/TreatmentCasesPage.test.tsx @@ -142,14 +142,14 @@ describe('صفحهٔ پرونده‌های درمان', () => { }); /** تا وقتی جلسه‌ای انجام نشده، برنامه را نشان می‌دهیم نه هیچ. */ - it('اگر هنوز انجام نشده، اپراتورِ اختصاص‌یافته را نشان می‌دهد', async () => { + it('اگر هنوز انجام نشده، پرسنلِ اختصاص‌یافته را نشان می‌دهد', async () => { mockList([caseRow({ assigned_staff: [{ uuid: 'st-2', name: 'پرسنل۲' }] })]); renderWithProviders(); await screen.findByText('محمد رسولی'); fireEvent.click(screen.getByLabelText('نمایش کارتی')); - expect(await screen.findByText(/اپراتور: پرسنل۲/)).toBeInTheDocument(); + expect(await screen.findByText(/پرسنل: پرسنل۲/)).toBeInTheDocument(); }); /** نمای پیش‌فرض جدول است — همان الگوی صفحهٔ پرونده‌ها، با سوییچ به کارتی. */ diff --git a/assets/admin/pages/TreatmentCasesPage.tsx b/assets/admin/pages/TreatmentCasesPage.tsx index 2aba9493..d21fa502 100644 --- a/assets/admin/pages/TreatmentCasesPage.tsx +++ b/assets/admin/pages/TreatmentCasesPage.tsx @@ -354,7 +354,7 @@ function CaseCard({ item: c, onEdit }: { item: TreatmentCaseSummary; onEdit: () {c.supervisor && پزشک ناظر: {c.supervisor.name}} {operator !== null && ( operator.planned - ? اپراتور: {operator.text} (هنوز انجام نشده) + ? پرسنل: {operator.text} (هنوز انجام نشده) : انجام‌دهنده: {operator.text} )} {c.areas.length > 0 && نواحی: {c.areas.map((a) => a.name).join('، ')}}