>({
- queryKey: ['booking-doctors'],
- queryFn: () => api.get('/api/v1/my/clinic-doctors'),
- staleTime: 60_000,
- });
- const doctors = doctorsQuery.data?.data?.data ?? [];
-
const range = useMemo(() => {
const from = Math.floor(Date.now() / 1000);
return { from, to: from + Number(days) * DAY };
@@ -82,6 +78,8 @@ export default function ResourceBookingModal({ resource, onClose, onBooked }: {
const reasonText = result?.reason ? REASON_LABELS[result.reason] ?? result.reason : null;
const canSubmit = !!serviceUuid && !!pickedSlot && !!doctorUuid && !submitting;
+ // ناظر در API الزامی است؛ ردیفهای قدیمیای که پزشکشان حذف شده `supervisor: null` دارند.
+ const missingSupervisor = !resource.supervisor;
const submit = async () => {
if (!pickedSlot) return;
@@ -128,6 +126,12 @@ export default function ResourceBookingModal({ resource, onClose, onBooked }: {
>
}
>
+ {missingSupervisor && (
+
+ این منبع پزشک ناظر ندارد و نوبتی برایش ثبت نمیشود — از «منابع» ناظرش را تعیین کنید.
+
+ )}
+
{offeringsQuery.isLoading ? (
@@ -206,22 +210,14 @@ export default function ResourceBookingModal({ resource, onClose, onBooked }: {
)}
- {pickedSlot && (
-
-
-
({ value: d.uuid, label: d.name }))}
- value={doctorUuid || null}
- onChange={(v) => setDoctorUuid(v ? String(v) : '')}
- placeholder="پزشک انجامدهنده"
- height={40}
- />
-
- هر نوبت پزشک مسئول دارد؛ منبع میگوید کار روی چه دستگاهی انجام میشود.
-
+
+
+
پزشک ناظر: {resource.supervisor?.name ?? '—'}
+
+ نوبت به نام همین پزشک ثبت میشود. برای تغییرش، ناظرِ منبع را در «منابع» ویرایش کنید.
+
- )}
+
);
}
diff --git a/assets/admin/pages/AppointmentsPage.test.tsx b/assets/admin/pages/AppointmentsPage.test.tsx
index 0c4dd62f..ac7c6279 100644
--- a/assets/admin/pages/AppointmentsPage.test.tsx
+++ b/assets/admin/pages/AppointmentsPage.test.tsx
@@ -66,9 +66,11 @@ describe('AppointmentsPage — پروفایل کلینیک چندپزشکه', ()
] } });
if (url.includes('appointment-slots')) return Promise.resolve({ success: true, data: { sessions: [], empty_reason: 'holiday' } });
if (url.includes('/my/appointments')) return Promise.resolve({ success: true, data: [], meta: { totalRecords: 0, totalPages: 0, currentPage: 1 } });
+ // منابع زیر نظر پزشکاند؛ تب هر منبع فقط زیر ناظرِ خودش دیده میشود.
if (url.includes('/api/v1/resources')) return Promise.resolve({ success: true, data: [
- { uuid: 'r-1', name: 'اتاق ۱', type_name: 'اتاق درمان' },
- { uuid: 'r-2', name: 'لیزر CO2', type_name: 'دستگاه لیزر' },
+ { uuid: 'r-1', name: 'اتاق ۱', type_name: 'اتاق درمان', supervisor: { uuid: 'd1', name: 'دکتر محمدی' } },
+ { uuid: 'r-2', name: 'لیزر CO2', type_name: 'دستگاه لیزر', supervisor: { uuid: 'd1', name: 'دکتر محمدی' } },
+ { uuid: 'r-3', name: 'لیزر دکتر رضایی', type_name: 'دستگاه لیزر', supervisor: { uuid: 'd2', name: 'دکتر رضایی' } },
] });
return Promise.resolve({ success: true, data: [] });
});
@@ -85,10 +87,12 @@ describe('AppointmentsPage — پروفایل کلینیک چندپزشکه', ()
* منابع مثل پزشکان تب خودشان را دارند: نوبتِ «لیزر CO2» به دستگاه تعلق دارد، نه به
* پزشکی که پشتش ایستاده.
*/
- it('برای هر منبع فعال یک تب نشان میدهد', async () => {
+ it('فقط منابعِ تحت نظارت پزشکِ انتخابشده تب میگیرند', async () => {
renderWithProviders();
+ // پزشک اول (d1) خودکار انتخاب میشود، پس فقط منابع او دیده میشوند.
expect(await screen.findByText('لیزر CO2')).toBeInTheDocument();
expect(screen.getByText('اتاق ۱')).toBeInTheDocument();
+ expect(screen.queryByText('لیزر دکتر رضایی')).toBeNull();
});
/** تب فعال از URL خوانده میشود و فهرست با resource_uuid فیلتر میشود، نه doctor_uuid. */
diff --git a/assets/admin/pages/AppointmentsPage.tsx b/assets/admin/pages/AppointmentsPage.tsx
index af92c17d..09b5891e 100644
--- a/assets/admin/pages/AppointmentsPage.tsx
+++ b/assets/admin/pages/AppointmentsPage.tsx
@@ -527,6 +527,19 @@ export default function AppointmentsPage() {
const selectedResourceUuid = urlState.resource;
const { resources: bookableResources } = useResources({ active: '1' });
+ /**
+ * منابعی که پزشکِ انتخابشده ناظرشان است.
+ *
+ * وقتی خودِ تب منبع فعال است `selectedDoctorUuid` خالی میشود، پس ناظرِ همان منبع
+ * مبنا قرار میگیرد — وگرنه نوار منابع زیر پای کاربر خالی میشد.
+ */
+ const supervisorFilterUuid = selectedDoctorUuid
+ || bookableResources.find((r) => r.uuid === selectedResourceUuid)?.supervisor?.uuid
+ || '';
+ const supervisedResources = supervisorFilterUuid
+ ? bookableResources.filter((r) => r.supervisor?.uuid === supervisorFilterUuid)
+ : [];
+
const [bookingResource, setBookingResource] = useState(null);
const activeResource = bookableResources.find((r) => r.uuid === selectedResourceUuid) ?? null;
@@ -867,13 +880,13 @@ export default function AppointmentsPage() {
/>
)}
- {/* منابع مثل پزشکان تب خودشان را دارند: نوبتِ «لیزر CO2» به دستگاه تعلق دارد،
- نه به پزشکی که پشتش ایستاده. */}
- {bookableResources.length > 0 && (
+ {/* منابعِ همین پزشک، نه همهٔ منابع: ارتباط پزشک↔منبع روی خودِ منبع تعریف شده
+ (پزشک ناظر)، پس کاربر بین نوبتهای پزشک و دستگاههای تحت نظرش جابهجا میشود. */}
+ {supervisedResources.length > 0 && (
- منابع
+ منابعِ این پزشک
({ uuid: r.uuid, name: r.name }))}
+ doctors={supervisedResources.map((r) => ({ uuid: r.uuid, name: r.name }))}
selected={selectedResourceUuid}
onSelect={selectResource}
showAll={false}