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('، ')}}