From 3cc4a594591e68935c90256b8ae2939b46567d80 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sat, 25 Jul 2026 18:45:09 +0330 Subject: [PATCH] feat(secretary): enhance functionality for secretary role in appointments management --- assets/admin/pages/AppointmentsPage.test.tsx | 54 ++++++++++++++++++++ assets/admin/pages/AppointmentsPage.tsx | 27 +++++----- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/assets/admin/pages/AppointmentsPage.test.tsx b/assets/admin/pages/AppointmentsPage.test.tsx index 660a67f9..38720b38 100644 --- a/assets/admin/pages/AppointmentsPage.test.tsx +++ b/assets/admin/pages/AppointmentsPage.test.tsx @@ -86,3 +86,57 @@ describe('AppointmentsPage — پروفایل کلینیک چندپزشکه', () expect(calledSlotsForD1).toBe(true); }); }); + +describe('AppointmentsPage — منشی', () => { + /** + * منشی در هر دو ساختار باید تایم‌لاین ببیند. لیست پزشکانِ مجاز از اندپوینت + * احرازشدهٔ /my/clinic-doctors می‌آید (نه لیست عمومی کلینیک). + */ + function mockSecretary(doctors: { uuid: string; name: string }[], scope: string | null) { + useAuthStore.setState({ + primaryRole: 'secretary', + dbUuid: scope === 'clinic' ? 'clinic1' : 'doc1', + context: scope ? { type: scope, scope } : null, + } as any); + get.mockImplementation((url?: string) => { + if (typeof url !== 'string') return Promise.resolve({ success: true, data: [] }); + if (url.includes('today-stats')) return Promise.resolve({ success: true, data: { total: 11, completed: 2, waiting: 7, cancelled: 2 } }); + if (url.includes('/my/clinic-doctors')) return Promise.resolve({ success: true, data: { data: doctors } }); + 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 } }); + return Promise.resolve({ success: true, data: [] }); + }); + } + + beforeEach(() => get.mockReset()); + + it('منشیِ پزشک مستقل: همان پزشک خودکار انتخاب و تایم‌لاین بارگذاری می‌شود', async () => { + mockSecretary([{ uuid: 'doc1', name: 'دکتر موسوی' }], null); + renderWithProviders(); + + expect(await screen.findByText('این روز تعطیل است')).toBeInTheDocument(); + expect(screen.queryByText('برای نمایش زمانبندی، ابتدا یک پزشک انتخاب کنید')).toBeNull(); + const calledSlots = get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('appointment-slots') && c[0].includes('doctor_uuid=doc1')); + expect(calledSlots).toBe(true); + }); + + it('منشیِ کلینیک: تب پزشکانِ تخصیص‌یافته و انتخاب خودکار اولی', async () => { + mockSecretary([{ uuid: 'd1', name: 'دکتر محمدی' }, { uuid: 'd2', name: 'دکتر رضایی' }], 'clinic'); + renderWithProviders(); + + expect(await screen.findByText('دکتر محمدی')).toBeInTheDocument(); + expect(screen.getByText('دکتر رضایی')).toBeInTheDocument(); + await screen.findByText('این روز تعطیل است'); + const calledSlotsForD1 = get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('appointment-slots') && c[0].includes('doctor_uuid=d1')); + expect(calledSlotsForD1).toBe(true); + }); + + it('منشی هرگز از لیست عمومی کلینیک نمی‌خواند', async () => { + mockSecretary([{ uuid: 'doc1', name: 'دکتر موسوی' }], null); + renderWithProviders(); + await screen.findByText('این روز تعطیل است'); + + const usedPublicList = get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('/clinic/doctor-list/')); + expect(usedPublicList).toBe(false); + }); +}); diff --git a/assets/admin/pages/AppointmentsPage.tsx b/assets/admin/pages/AppointmentsPage.tsx index 02849383..03ae3717 100644 --- a/assets/admin/pages/AppointmentsPage.tsx +++ b/assets/admin/pages/AppointmentsPage.tsx @@ -495,9 +495,11 @@ export default function AppointmentsPage() { const isClinic = primaryRole === 'clinic'; const isDoctor = primaryRole === 'doctor'; const isRepresentation = primaryRole === 'representation'; - // منشیِ محیطِ کلینیک باید مثل کلینیک چندپزشکه رفتار کند: تب پزشکان + تایم‌لاین. - // dbUuid در این محیط uuid کلینیک است (نه پزشک) — همان مبنای clinic/doctor-list. - const isClinicScopedSecretary = primaryRole === 'secretary' && scope === 'clinic'; + // منشی در هر دو ساختار باید تایم‌لاین ببیند: کلینیک چندپزشکه (تب پزشکانِ + // تخصیص‌یافته) و پزشک مستقل (همان یک پزشک). لیست در هر دو حالت از اندپوینتِ + // احرازشدهٔ /my/clinic-doctors می‌آید که خودش هر دو سناریو را resolve می‌کند. + const isSecretary = primaryRole === 'secretary'; + const isClinicScopedSecretary = isSecretary && scope === 'clinic'; // مجوزهای منشی روی نوبت‌ها؛ برای owner/پزشک همیشه true. const { can } = usePermissions(); const canCreateAppt = can('appointments', 'create'); @@ -552,15 +554,15 @@ export default function AppointmentsPage() { // ── Clinic doctors (authoritative list for tabs) const clinicDoctorsQuery = useQuery>({ - queryKey: ['clinic-doctors', dbUuid, isClinicScopedSecretary], - // منشی از اندپوینتِ احرازشده می‌گیرد تا فقط پزشکانِ تخصیص‌یافته‌اش بیایند؛ - // کلینیک/ادمین از لیستِ کاملِ عمومیِ کلینیک. + queryKey: ['clinic-doctors', dbUuid, isSecretary], + // منشی از اندپوینتِ احرازشده می‌گیرد تا فقط پزشکانِ تخصیص‌یافته‌اش بیایند — + // چه کلینیک چندپزشکه و چه پزشک مستقل؛ کلینیک/ادمین از لیستِ عمومیِ کلینیک. queryFn: () => api.get( - isClinicScopedSecretary + isSecretary ? '/api/v1/my/clinic-doctors' : `/api/v1/clinic/doctor-list/${dbUuid}`, ), - enabled: (isClinic || isClinicScopedSecretary) && !!dbUuid, + enabled: isSecretary || (isClinic && !!dbUuid), }); const clinicDoctorsList = clinicDoctorsQuery.data?.data?.data ?? []; @@ -586,16 +588,17 @@ export default function AppointmentsPage() { // نوع پروفایل: کلینیک چندپزشکه (تب دکترها + مدیریت چند پزشک) در برابر پزشک مستقل. // نقش clinic/admin = چندپزشکه؛ نقش doctor (حتی مهمانِ کلینیک) = مستقل، فقط برنامهٔ خودش. - const isMultiDoctorClinic = isClinic || isAdmin || isClinicScopedSecretary; + const isMultiDoctorClinic = isClinic || isAdmin || isSecretary; const showDoctorTabs = isMultiDoctorClinic && doctors.length >= 1; const showDoctorCol = isAdmin && !selectedDoctorUuid; - // در کلینیک، اولین دکتر به‌صورت پیش‌فرض انتخاب می‌شود تا زمانبندی مثل طرح پر باشد. + // کلینیک و منشی: اولین پزشکِ در دسترس پیش‌فرض انتخاب می‌شود تا تایم‌لاین خالی نماند. + // برای منشیِ پزشک مستقل این تنها راه انتخاب است (تبِ پزشک هم یک گزینه بیشتر ندارد). useEffect(() => { - if ((isClinic || isClinicScopedSecretary) && !selectedDoctorUuid && doctors.length > 0) { + if ((isClinic || isSecretary) && !selectedDoctorUuid && doctors.length > 0) { setSelectedDoctorUuid(doctors[0].uuid); } - }, [isClinic, isClinicScopedSecretary, selectedDoctorUuid, doctors]); + }, [isClinic, isSecretary, selectedDoctorUuid, doctors]); // ── محل نوبت‌دهی برای ادمین // ادمین context کلینیکی ندارد (useClinicContext → null)؛ بدون clinic_uuid فقط برنامهٔ