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