From f41852b1688a8fbe57023a7c92adc12fac1f4092 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Wed, 15 Jul 2026 16:07:04 +0330 Subject: [PATCH] feat(appointments): profile-aware doctor tabs + toolbar to match tauri turns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Multi-doctor clinic profile (clinic/admin) shows the doctor tabs for multi-doctor management; clinic auto-selects the first doctor so the timeline loads immediately (as in the reference). Admin keeps the «همه» tab. - Independent doctor profile (doctor role, incl. clinic-scoped guest) shows no tabs and only its own schedule — no clinic/doctor selection. - Toolbar reordered to match the reference: date + service select («سرویس مورد نظر را انتخاب کنید...») + view toggle on the right; filter + «افزودن نوبت» on the left. The in-toolbar doctor picker is removed (doctor selection is via tabs); the personnel select is replaced by a service filter bound to the existing itemUuid filter (client-side, no new endpoint). Tests: added clinic-profile tests (tabs shown, no «همه», first doctor auto-selected) and independent-doctor test (no tabs, service filter shown). No API endpoints changed. Co-Authored-By: Claude Opus 4.8 (1M context) --- assets/admin/pages/AppointmentsPage.test.tsx | 40 ++++++++ assets/admin/pages/AppointmentsPage.tsx | 100 ++++++++++--------- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/assets/admin/pages/AppointmentsPage.test.tsx b/assets/admin/pages/AppointmentsPage.test.tsx index cac242b4..2a58cabe 100644 --- a/assets/admin/pages/AppointmentsPage.test.tsx +++ b/assets/admin/pages/AppointmentsPage.test.tsx @@ -38,4 +38,44 @@ describe('AppointmentsPage — طرح نوبت‌ها', () => { // نمای پیش‌فرض زمانبندی است و پزشک (نقش doctor) از قبل انتخاب شده expect(await screen.findByText('این روز تعطیل است')).toBeInTheDocument(); }); + + it('independent doctor profile does NOT show doctor tabs', async () => { + renderWithProviders(); + await screen.findByText('این روز تعطیل است'); + expect(screen.queryByText('همه')).toBeNull(); + // برچسب فیلتر سرویس دیده می‌شود (مطابق طرح) + expect(screen.getByText('سرویس مورد نظر را انتخاب کنید...')).toBeInTheDocument(); + }); +}); + +describe('AppointmentsPage — پروفایل کلینیک چندپزشکه', () => { + beforeEach(() => { + get.mockReset(); + 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: 'دکتر رضایی' }, + ] } }); + if (url.includes('appointment-slots')) return Promise.resolve({ success: true, data: { sessions: [] } }); + if (url.includes('/my/appointments')) return Promise.resolve({ success: true, data: [], meta: { totalRecords: 0, totalPages: 0, currentPage: 1 } }); + return Promise.resolve({ success: true, data: [] }); + }); + }); + + it('shows the doctor tabs (multi-doctor management) without the «همه» tab', async () => { + renderWithProviders(); + 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(); + await screen.findByText('دکتر محمدی'); + // اسلات‌ها برای اولین دکتر (d1) درخواست می‌شوند + 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); + }); }); diff --git a/assets/admin/pages/AppointmentsPage.tsx b/assets/admin/pages/AppointmentsPage.tsx index cee26e9a..3b2cc235 100644 --- a/assets/admin/pages/AppointmentsPage.tsx +++ b/assets/admin/pages/AppointmentsPage.tsx @@ -15,7 +15,6 @@ import Pagination from '../components/ui/Pagination'; import AppointmentFiltersModal, { applyAppointmentFilters, EMPTY_FILTERS } from '../components/AppointmentFiltersModal'; import type { AppointmentFilters } from '../components/AppointmentFiltersModal'; import PersianCalendar from '../components/ui/PersianCalendar'; -import SearchableSelect from '../components/ui/SearchableSelect'; // اجزای طرح نوبت‌های tauri import TurnsStatInfo from '../components/appointments/TurnsStatInfo'; import TurnsViewToggle from '../components/appointments/TurnsViewToggle'; @@ -264,8 +263,27 @@ export default function AppointmentsPage() { return Array.from(map.entries()).map(([uuid, name]) => ({ uuid, name })); }, [appointments, clinicDoctorsList]); - const showDoctorTabs = (isClinic || isAdmin) && doctors.length >= 2; - const showDoctorCol = isAdmin || (isClinic && !selectedDoctorUuid); + // سرویس‌های موجود در نوبت‌های امروز (برای فیلتر «سرویس مورد نظر...»). + const serviceOptions = React.useMemo(() => { + const map = new Map(); + appointments.forEach(a => { + if (a.service_item?.uuid && a.service_item?.name) map.set(a.service_item.uuid, a.service_item.name); + }); + return Array.from(map.entries()).map(([uuid, name]) => ({ uuid, name })); + }, [appointments]); + + // نوع پروفایل: کلینیک چندپزشکه (تب دکترها + مدیریت چند پزشک) در برابر پزشک مستقل. + // نقش clinic/admin = چندپزشکه؛ نقش doctor (حتی مهمانِ کلینیک) = مستقل، فقط برنامهٔ خودش. + const isMultiDoctorClinic = isClinic || isAdmin; + const showDoctorTabs = isMultiDoctorClinic && doctors.length >= 1; + const showDoctorCol = isAdmin && !selectedDoctorUuid; + + // در کلینیک، اولین دکتر به‌صورت پیش‌فرض انتخاب می‌شود تا زمانبندی مثل طرح پر باشد. + useEffect(() => { + if (isClinic && !selectedDoctorUuid && doctors.length > 0) { + setSelectedDoctorUuid(doctors[0].uuid); + } + }, [isClinic, selectedDoctorUuid, doctors]); // ── Slots query (timeline) const slotsQueryKey = ['appt-slots', selectedDoctorUuid, selectedDate]; @@ -342,23 +360,20 @@ export default function AppointmentsPage() {
- {/* افزودن نوبت → صفحهٔ کامل */} - {!isRepresentation && ( - - )} + {/* سمت راست: تاریخ + سرویس + سوییچ نما (مطابق طرح) */} + - setFilters(f => ({ ...f, staffUuid: v }))} /> + setFilters(f => ({ ...f, itemUuid: v }))} + /> + + +
+ + {/* سمت چپ: فیلتر + افزودن نوبت */} - - - {/* انتخاب پزشک (admin / clinic) */} - {!isDoctor && ( -
- ({ value: d.uuid, label: d.name }))} - value={selectedDoctorUuid || null} - onChange={(v) => setSelectedDoctorUuid(v ? String(v) : '')} - placeholder="انتخاب پزشک..." - isClearable - height={36} - /> -
+ {!isRepresentation && ( + )} - -
- -
{/* کارت اصلی — تب دکترها (هدر) + محتوا */} @@ -399,7 +408,7 @@ export default function AppointmentsPage() { borderRadius: 'var(--r)', overflow: 'hidden', }}> {showDoctorTabs && ( - + )}
{viewMode === 'table' ? ( @@ -458,16 +467,15 @@ export default function AppointmentsPage() { ); } -/** فیلتر پرسنل نوار ابزار — ردیف‌های بارگذاری‌شدهٔ روز را بر اساس پرسنل فیلتر می‌کند. */ -function StaffFilterSelect({ value, onChange }: { value: string; onChange: (v: string) => void }) { - const staffQ = useQuery>({ - queryKey: ['staff-list'], queryFn: () => api.get('/api/v1/staff'), - }); +/** فیلتر سرویس نوار ابزار — ردیف‌های بارگذاری‌شدهٔ روز را بر اساس سرویس فیلتر می‌کند. */ +function ServiceFilterSelect({ value, options, onChange }: { + value: string; options: { uuid: string; name: string }[]; onChange: (v: string) => void; +}) { return ( - onChange(e.target.value)} + style={{ height: 44, borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', fontSize: 13, fontFamily: 'inherit', padding: '0 12px', minWidth: 280 }}> + + {options.map(s => )} ); }