import { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { CubeIcon, UserGroupIcon } from '@heroicons/react/24/outline'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import { useAuthStore } from '../stores/authStore'; import { usePermissions } from '../hooks/usePermissions'; import { useUrlState } from '../hooks/useUrlState'; import { useResources } from '../hooks/useResources'; import SettingsLayout from '../components/layout/SettingsLayout'; import { ScheduleSection } from '../components/schedule/ScheduleSection'; import ResourceWorkingHoursPanel from '../components/resources/ResourceWorkingHoursPanel'; import ResourceExceptionsPanel from '../components/resources/ResourceExceptionsPanel'; import FreeVisitPrice from '../components/FreeVisitPrice'; import PageHeader from '../components/ui/PageHeader'; import type { ClinicDoctorItem } from '../components/ClinicDoctorsManager'; const SCOPES = [ { id: 'doctors', label: 'پزشکان' }, { id: 'resources', label: 'منابع' }, ] as const; type Scope = typeof SCOPES[number]['id']; /** * تنظیمات نوبت‌دهی کلینیک — یک تب به ازای هر پزشک، و یک تب به ازای هر منبع. * * منبع در مدل Resource-First واحدِ ظرفیت است و ساعت کاری و تعطیلات خودش را دارد، پس * دقیقاً همان‌جایی مدیریت می‌شود که برنامهٔ پزشک — نه در یک صفحهٔ جدا. تب پزشک همان * `ScheduleSection` پنل پزشک مستقل است و تب منبع همان پنل‌های صفحهٔ منبع؛ هیچ‌کدام * نسخهٔ دومی ندارند. */ function ClinicAppointmentSettingsContent() { const { dbUuid, context, availableContexts } = useAuthStore(); const { can } = usePermissions(); // منشیِ بدون مجوزِ ویرایشِ تنظیمات نوبت‌دهی، فقط مشاهده می‌کند. const apptReadOnly = !can('appointment_settings', 'update'); // انتخاب‌ها در URL می‌نشینند تا «بازگشت» و رفرش همان تب را برگردانند. const [urlState, setUrlState] = useUrlState({ scope: 'doctors', doctor: '', resource: '' }); const scope = (SCOPES.some((s) => s.id === urlState.scope) ? urlState.scope : 'doctors') as Scope; // کاربری که هم پزشک است هم مالک کلینیک، dbUuid‌اش ممکن است uuid پزشک باشد. const clinicUuid = useMemo(() => { if (context?.type === 'clinic') return dbUuid; return availableContexts.find(c => c.type === 'clinic')?.db_uuid ?? null; }, [context, dbUuid, availableContexts]); const doctorsQ = useQuery({ queryKey: ['clinic-doctors', clinicUuid], queryFn: () => api.get>(`/api/v1/clinic/doctor-list/${clinicUuid}`), enabled: !!clinicUuid, }); const doctorList: ClinicDoctorItem[] = useMemo(() => { const raw = doctorsQ.data?.data; return (raw as any)?.data ?? raw ?? []; }, [doctorsQ.data]); const { resources, loading: resourcesLoading } = useResources({ active: '1' }); const selectedDoctor = doctorList.find(d => d.uuid === urlState.doctor) ?? doctorList[0] ?? null; const selectedResource = resources.find(r => r.uuid === urlState.resource) ?? resources[0] ?? null; if (!clinicUuid) { return (

{dbUuid ? 'کلینیکی برای این حساب کاربری یافت نشد' : 'در حال بارگذاری اطلاعات کلینیک...'}

); } // هویت موردِ انتخاب‌شده فقط یک بار گفته می‌شود — در توضیح هدر. پیش‌تر همین جمله // در تب فعال، در زیرعنوان و در یک کارتِ جداگانه سه بار تکرار می‌شد. const description = scope === 'doctors' ? (selectedDoctor ? `تنظیمات نوبت‌دهی ${selectedDoctor.name}` : 'تنظیمات نوبت‌دهی پزشکان کلینیک') : (selectedResource ? `تنظیمات نوبت‌دهی ${selectedResource.name} · ${selectedResource.type_name}` : 'تنظیمات نوبت‌دهی منابع کلینیک'); return (
{/* دو انتخابگر پشت سر هم می‌مانند — «نما» بعد «مورد». در اسلات action هدر، سوییچر به لبهٔ مقابلِ صفحه می‌افتاد و از تبی که کنترل می‌کند جدا می‌شد. */}
نما
{SCOPES.map((s) => ( ))}
{scope === 'doctors' ? ( setUrlState({ doctor: uuid })} /> ) : ( setUrlState({ resource: uuid })} /> )}
); } /** * نوار انتخابِ پزشک/منبع. * * بدون کارتِ دورش: تنها محتوایش یک گروه pill بود و کارتِ تمام‌عرض، ~۴۵۰px فضای * خالی می‌ساخت. لیبل قابل‌مشاهده هم لازم است، وگرنه نوار برای screen reader یک * ردیف دکمهٔ بی‌عنوان است. */ function TabBar({ items, selected, onSelect, label, labelId }: { items: T[]; selected: T | null; onSelect: (uuid: string) => void; label: string; labelId: string; }) { return (
{label}
{items.map((item) => ( ))}
); } /** حالت خالی. کلاس `empty` در `styles.css` تعریف نشده بود، پس چیدمان اینجاست. */ function EmptyState({ icon, message, action }: { icon: React.ReactNode; message: string; action: React.ReactNode; }) { return (
{icon}

{message}

{action}
); } function DoctorsScope({ loading, doctors, selected, clinicUuid, readOnly, onSelect }: { loading: boolean; doctors: ClinicDoctorItem[]; selected: ClinicDoctorItem | null; clinicUuid: string; readOnly: boolean; onSelect: (uuid: string) => void; }) { if (loading) return

در حال بارگذاری پزشکان...

; if (doctors.length === 0) { return ( } message="هیچ پزشکی به این کلینیک متصل نیست" action={مدیریت پزشکان کلینیک} /> ); } return ( <> {/* key اجباری است: بدون آن state ویرایشِ برنامه بین پزشکان نشت می‌کند */} {selected && (
)} ); } function ResourcesScope({ loading, resources, selected, canUpdate, onSelect }: { loading: boolean; resources: { uuid: string; name: string; type_name: string }[]; selected: { uuid: string; name: string; type_name: string } | null; canUpdate: boolean; onSelect: (uuid: string) => void; }) { if (loading) return

در حال بارگذاری منابع...

; if (resources.length === 0) { return ( } message="هنوز منبعی تعریف نشده است" action={تنظیمات ← منابع} /> ); } return ( <> {/* همان دلیل تب پزشک: بدون key، شیفتِ نیمه‌ویرایش‌شده به منبع بعدی می‌چسبد. */} {selected && (
)} ); } export default function ClinicAppointmentSettingsPage() { return ( ); }