feat(appointments): only doctors with a weekly schedule get a tab

my/clinic-doctors now reports has_schedule per doctor, and the appointments page
builds tabs from it. A doctor with no working days had a tab that could only ever
show an empty timeline.

The flag is resolved with one query for the whole list rather than one per
doctor. Clinic owners now read this authenticated endpoint too instead of the
public clinic doctor-list, which is where the flag lives; admin keeps the public
list and, with no flag present, hides nobody.

Also repairs fallout from making the resource supervisor mandatory: four test
classes build resources through their own helpers and were failing with 422. The
supervisorFor helper moved to ApiTestCase so all domains share one, rather than
copying it per suite. Full backend suite is green again (1306 tests) — the
previous commit only ran tests/Resource and missed this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-03 12:42:42 +03:30
co-authored by Claude Opus 5
parent a63abc31be
commit fd59eb57a3
10 changed files with 107 additions and 45 deletions
+16 -3
View File
@@ -61,9 +61,14 @@ describe('AppointmentsPage — پروفایل کلینیک چندپزشکه', ()
useAuthStore.setState({ primaryRole: 'clinic', dbUuid: 'clinic1' } as any);
get.mockImplementation((url: string) => {
if (url.includes('today-stats')) return Promise.resolve({ success: true, data: { total: 5, completed: 1, waiting: 3, cancelled: 1 } });
if (url.includes('/clinic/doctor-list/')) return Promise.resolve({ success: true, data: { data: [
{ uuid: 'd1', name: 'دکتر محمدی' }, { uuid: 'd2', name: 'دکتر رضایی' },
] } });
// کلینیک هم از اندپوینت احرازشده می‌خواند؛ `has_schedule` مبنای ساختن تب است.
if (url.includes('/my/clinic-doctors') || url.includes('/clinic/doctor-list/')) {
return Promise.resolve({ success: true, data: { data: [
{ uuid: 'd1', name: 'دکتر محمدی', has_schedule: true },
{ uuid: 'd2', name: 'دکتر رضایی', has_schedule: true },
{ uuid: 'd3', name: 'دکتر بی‌برنامه', has_schedule: false },
] } });
}
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 } });
// منابع زیر نظر پزشک‌اند؛ تب هر منبع فقط زیر ناظرِ خودش دیده می‌شود.
@@ -108,6 +113,14 @@ describe('AppointmentsPage — پروفایل کلینیک چندپزشکه', ()
expect(calls.some((u: string) => u.includes('doctor_uuid='))).toBe(false);
});
/** پزشکی که در تنظیمات نوبت‌دهی روز کاری تعریف نکرده تب نمی‌گیرد. */
it('پزشک بدون برنامهٔ کاری تب نمی‌گیرد', async () => {
renderWithProviders(<AppointmentsPage />);
expect(await screen.findByText('دکتر محمدی')).toBeInTheDocument();
expect(screen.getByText('دکتر رضایی')).toBeInTheDocument();
expect(screen.queryByText('دکتر بی‌برنامه')).toBeNull();
});
it('auto-selects the first doctor so the timeline loads its slots', async () => {
renderWithProviders(<AppointmentsPage />);
await screen.findByText('دکتر محمدی');
+11 -6
View File
@@ -595,12 +595,13 @@ export default function AppointmentsPage() {
const pagedAppointments = filteredAppointments.slice((tablePage - 1) * TABLE_PAGE_SIZE, tablePage * TABLE_PAGE_SIZE);
// ── Clinic doctors (authoritative list for tabs)
const clinicDoctorsQuery = useQuery<ApiResponse<{ data: { uuid: string; name: string }[] }>>({
queryKey: ['clinic-doctors', dbUuid, isSecretary],
// منشی از اندپوینتِ احرازشده می‌گیرد تا فقط پزشکانِ تخصیص‌یافته‌اش بیایند —
// چه کلینیک چندپزشکه و چه پزشک مستقل؛ کلینیک/ادمین از لیستِ عمومیِ کلینیک.
const clinicDoctorsQuery = useQuery<ApiResponse<{ data: { uuid: string; name: string; has_schedule?: boolean }[] }>>({
queryKey: ['clinic-doctors', dbUuid, isSecretary, isClinic],
// منشی و کلینیک از اندپوینتِ احرازشده می‌گیرند: هم فقط پزشکانِ مجاز را می‌دهد و هم
// `has_schedule` را، که مبنای ساختن تب است. ادمین از لیستِ کلینیکِ انتخاب‌شده
// می‌خواند (آن اندپوینت نقش ادمین را پوشش نمی‌دهد) و آنجا فلگ نمی‌آید.
queryFn: () => api.get(
isSecretary
isSecretary || isClinic
? '/api/v1/my/clinic-doctors'
: `/api/v1/clinic/doctor-list/${dbUuid}`,
),
@@ -610,7 +611,11 @@ export default function AppointmentsPage() {
const doctors = React.useMemo(() => {
const map = new Map<string, string>();
clinicDoctorsList.forEach(d => map.set(d.uuid, d.name));
// پزشکِ بدون برنامهٔ کاری تب نمی‌گیرد — تبش جز یک تایم‌لاین همیشه‌خالی چیزی ندارد.
// نبودِ فیلد (مسیر ادمین) یعنی «نمی‌دانیم»، پس پنهان نمی‌شود.
clinicDoctorsList
.filter(d => d.has_schedule !== false)
.forEach(d => map.set(d.uuid, d.name));
appointments.forEach(a => {
if (a.doctor_uuid && a.doctor_name && !map.has(a.doctor_uuid)) {
map.set(a.doctor_uuid, a.doctor_name);