import React, { useMemo, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; import { UserGroupIcon, HeartIcon, BuildingOffice2Icon, CalendarDaysIcon, CreditCardIcon, ArrowPathIcon, BellAlertIcon, ChatBubbleLeftEllipsisIcon, ClockIcon, StarIcon, UserIcon, CheckIcon, XMarkIcon, } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import { formatNumber, formatRial, formatDateTime } from '../lib/utils'; import { useAuthStore } from '../stores/authStore'; import InviteDoctorModal from '../components/ui/InviteDoctorModal'; // ── Shared Status Maps ──────────────────────────────────────────────────── const APPT_LABEL: Record = { waiting_for_payment: 'انتظار پرداخت', reserved: 'رزرو شده', checked_in: 'ورود به مطب', waiting: 'صف انتظار', in_progress: 'در حال ویزیت', visited: 'ویزیت شده', completed: 'تکمیل شده', cancelled_by_doctor: 'لغو پزشک', cancelled_by_user: 'لغو بیمار', auto_cancel_unpaid: 'لغو خودکار', no_show: 'غیبت', }; const APPT_COLOR: Record = { waiting_for_payment: '#f59e0b', reserved: '#3b82f6', checked_in: '#6366f1', waiting: '#f97316', in_progress: '#8b5cf6', visited: '#10b981', completed: '#22c55e', cancelled_by_doctor: '#ef4444', cancelled_by_user: '#f43f5e', auto_cancel_unpaid: '#94a3b8', no_show: '#64748b', }; const APPT_CLS: Record = { waiting_for_payment: 'amber', reserved: 'blue', checked_in: 'violet', waiting: 'amber', in_progress: 'violet', visited: 'green', completed: 'green', cancelled_by_doctor: 'red', cancelled_by_user: 'red', auto_cancel_unpaid: 'gray', no_show: 'gray', }; const PAY_LABEL: Record = { pending: 'در انتظار', received: 'موفق', canceled: 'لغو شده', refund: 'استرداد', }; const PAY_COLOR: Record = { pending: '#f59e0b', received: '#22c55e', canceled: '#ef4444', refund: '#3b82f6', }; const PAY_CLS: Record = { pending: 'amber', received: 'green', canceled: 'red', refund: 'blue', }; // ── Shared SVG Charts ───────────────────────────────────────────────────── function SvgLineChart({ data, color, h = 220 }: { data: number[]; color: string; h?: number }) { if (data.length < 2) return null; const w = 760, pad = 8; const max = Math.max(...data) * 1.15 || 1; const stepX = (w - pad * 2) / (data.length - 1); const pts: [number, number][] = data.map((v, i) => [ pad + i * stepX, h - pad - (v / max) * (h - pad * 2 - 18), ]); const d = pts.map((p, i) => { if (i === 0) return `M${p[0]},${p[1]}`; const prev = pts[i - 1]; const cx = (prev[0] + p[0]) / 2; return `C${cx},${prev[1]} ${cx},${p[1]} ${p[0]},${p[1]}`; }).join(' '); const area = `${d} L${pts[pts.length - 1][0]},${h - pad} L${pts[0][0]},${h - pad} Z`; const gid = 'lg' + color.replace(/[^a-z0-9]/gi, ''); return ( {[0.25, 0.5, 0.75, 1].map((g, i) => ( ))} {pts.map((p, i) => i % 4 === 0 && ( ))} ); } function SvgDonut({ data, size = 190 }: { data: { value: number; color: string; label: string }[]; size?: number }) { const total = data.reduce((s, d) => s + d.value, 0); if (!total) return null; const r = size / 2 - 16; const c = 2 * Math.PI * r; let off = 0; return ( {data.map((d, i) => { const frac = d.value / total; const seg = ( ); off += frac; return seg; })} مجموع {formatNumber(total)} ); } function SvgHBars({ data }: { data: { label: string; value: number }[] }) { const max = Math.max(...data.map(d => d.value)) || 1; return (
{data.map((d, i) => (
{d.label}
{formatNumber(d.value)}
))}
); } // ── Shared UI Pieces ────────────────────────────────────────────────────── function AvatarEl({ initials, hue = 222, size }: { initials: string; hue?: number; size?: 'sm' | 'lg' }) { const cls = 'avatar' + (size === 'sm' ? ' sm' : size === 'lg' ? ' lg' : ''); return (
{initials}
); } interface MiniRow { title: string; sub: string; meta: string; badgeLabel: string; badgeCls: string; initials: string; hue: number; } function MiniList({ title, to, rows, loading }: { title: string; to: string; rows: MiniRow[]; loading?: boolean }) { return (

{title}

همه
{loading ? (
{Array.from({ length: 5 }).map((_, i) => (
))}
) : !rows.length ? (

موردی ثبت نشده

) : (
{rows.map((r, i) => (
{r.title} {r.sub}
{r.badgeLabel}
{r.meta}
))}
)}
); } function KpiSkeleton() { return (
); } function LoadingSkeleton() { return (
{Array.from({ length: 4 }).map((_, i) => )}
); } // ── Appointments Table (shared) ──────────────────────────────────────────── interface ApptRow { uuid: string; patient_name: string | null; patient_mobile?: string; slot_start: number; status: string; } function TodayAppointmentsTable({ appts, loading }: { appts: ApptRow[]; loading: boolean }) { if (loading) return
; if (!appts.length) return

نوبتی برای امروز ثبت نشده

; return (
{appts.map((a, i) => ( ))}
بیمار ساعت وضعیت
{a.patient_name || a.patient_mobile || '—'} {new Date(a.slot_start * 1000).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' })} {APPT_LABEL[a.status] ?? a.status}
); } // ── Admin Dashboard ─────────────────────────────────────────────────────── interface AdminStats { total_users: number; active_doctors: number; total_doctors: number; total_clinics: number; today_appointments: number; total_appointments: number; today_payments_count: number; today_payments_amount: number; total_payments_amount: number; pending_comments: number; pending_settlements: number; this_month_revenue: number; this_month_appointments: number; } interface AdminCharts { appointments_by_day: { date: number; count: number }[]; revenue_by_day: { date: number; amount_rials: number }[]; appointment_status: { status: string; count: number }[]; top_specialties: { name: string; count: number }[]; subscription_sales_by_plan?: { plan: string; count: number; total_rials: number }[]; } interface AdminRecent { appointments: { uuid: string; slot_start: string; status: string; doctor_name: string; user_mobile: string; user_name: string | null; created_at: string }[]; payments: { uuid: string; amount: number; status: string; gateway: string; user_mobile: string; user_name: string | null; created_at: string }[]; users: { uuid: string; mobile: string; name: string | null; email: string | null; created_at: string }[]; } type DatePreset = 'this_week' | 'this_month' | '3_months'; function getRange(preset: DatePreset): { from: number; to: number } { const now = Math.floor(Date.now() / 1000); if (preset === 'this_week') { const d = new Date(); d.setHours(0, 0, 0, 0); const day = d.getDay(); // 0=sun, 6=sat const daysToSat = (day + 1) % 7; const satMs = d.getTime() - daysToSat * 86400000; return { from: Math.floor(satMs / 1000), to: now }; } if (preset === 'this_month') { const d = new Date(); d.setDate(1); d.setHours(0, 0, 0, 0); return { from: Math.floor(d.getTime() / 1000), to: now }; } return { from: now - 90 * 86400, to: now }; } function AdminDashboard() { const [chartMode, setChartMode] = useState<'appts' | 'rev'>('appts'); const [preset, setPreset] = useState('this_month'); const range = getRange(preset); const statsQ = useQuery({ queryKey: ['dashboard-stats'], queryFn: () => api.get>('/api/v1/admin/dashboard/stats'), staleTime: 60_000 }); const chartsQ = useQuery({ queryKey: ['dashboard-charts', preset], queryFn: () => api.get>(`/api/v1/admin/dashboard/charts?from=${range.from}&to=${range.to}`), staleTime: 120_000 }); const recentQ = useQuery({ queryKey: ['dashboard-recent'], queryFn: () => api.get>('/api/v1/admin/dashboard/recent'), staleTime: 30_000 }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const stats = useMemo(() => (statsQ.data?.data as any)?.data ?? statsQ.data?.data, [statsQ.data]); // eslint-disable-next-line @typescript-eslint/no-explicit-any const charts = useMemo(() => (chartsQ.data?.data as any)?.data ?? chartsQ.data?.data, [chartsQ.data]); // eslint-disable-next-line @typescript-eslint/no-explicit-any const recent = useMemo(() => (recentQ.data?.data as any)?.data ?? recentQ.data?.data, [recentQ.data]); const fn = (n?: number) => n !== undefined ? formatNumber(n) : '—'; const fr = (n?: number) => n !== undefined ? formatRial(n) : '—'; const today = new Date().toLocaleDateString('fa-IR', { year: 'numeric', month: 'long', day: 'numeric' }); const isFetching = statsQ.isFetching || chartsQ.isFetching || recentQ.isFetching; const apptSeries = useMemo(() => charts?.appointments_by_day?.map(d => d.count) ?? [], [charts]); const revSeries = useMemo(() => charts?.revenue_by_day?.map(d => d.amount_rials) ?? [], [charts]); const donutData = useMemo(() => (charts?.appointment_status ?? []).slice(0, 7).map(s => ({ label: APPT_LABEL[s.status] ?? s.status, value: s.count, color: APPT_COLOR[s.status] ?? '#94a3b8', })), [charts]); const hbarsData = useMemo(() => (charts?.top_specialties ?? []).map(s => ({ label: s.name, value: s.count })), [charts]); const kpiCards = [ { label: 'کل کاربران', value: fn(stats?.total_users), hint: '', icon: UserGroupIcon, color: 'var(--violet)', bg: 'var(--violet-bg)' }, { label: 'پزشکان فعال', value: fn(stats?.active_doctors), hint: stats ? `از ${fn(stats.total_doctors)} پزشک` : '', icon: HeartIcon, color: 'var(--success)', bg: 'var(--success-bg)' }, { label: 'کلینیک‌ها', value: fn(stats?.total_clinics), hint: '', icon: BuildingOffice2Icon, color: 'var(--info)', bg: 'var(--info-bg)' }, { label: 'نوبت‌های امروز', value: fn(stats?.today_appointments), hint: stats ? `ماه جاری: ${fn(stats.this_month_appointments)}` : '', icon: CalendarDaysIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' }, { label: 'درآمد این ماه', value: fr(stats?.this_month_revenue), hint: 'تومان', icon: CreditCardIcon, color: 'var(--primary)', bg: 'var(--primary-soft)' }, { label: 'در انتظار بررسی', value: fn(stats ? stats.pending_comments + stats.pending_settlements : undefined), hint: stats ? `${fn(stats.pending_comments)} نظر · ${fn(stats.pending_settlements)} تسویه` : '', icon: BellAlertIcon, color: 'var(--danger)', bg: 'var(--danger-bg)' }, ]; const quickActions = [ { label: 'پزشکان', to: '/admin/doctors', icon: HeartIcon, color: 'var(--success)', bg: 'var(--success-bg)' }, { label: 'کلینیک‌ها', to: '/admin/clinics', icon: BuildingOffice2Icon, color: 'var(--info)', bg: 'var(--info-bg)' }, { label: 'نوبت‌ها', to: '/admin/appointments', icon: CalendarDaysIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' }, { label: 'پرداخت‌ها', to: '/admin/payments', icon: CreditCardIcon, color: 'var(--danger)', bg: 'var(--danger-bg)' }, { label: 'نظرات', to: '/admin/comments', icon: ChatBubbleLeftEllipsisIcon, color: 'var(--violet)', bg: 'var(--violet-bg)' }, { label: 'کاربران', to: '/admin/users', icon: UserGroupIcon, color: 'var(--primary)', bg: 'var(--primary-soft)' }, ]; const timelineEvents = useMemo(() => { if (!recent) return []; const evs: { title: string; sub: string; time: string; color: string }[] = []; (recent.appointments ?? []).slice(0, 4).forEach(a => { evs.push({ title: `نوبت ${APPT_LABEL[a.status] ?? a.status}`, sub: `${a.user_name || a.user_mobile} · دکتر ${a.doctor_name}`, time: a.created_at, color: APPT_COLOR[a.status] ?? '#94a3b8' }); }); (recent.payments ?? []).slice(0, 3).forEach(p => { evs.push({ title: `پرداخت ${PAY_LABEL[p.status] ?? p.status}`, sub: `${p.user_name || p.user_mobile} · ${formatRial(p.amount)}`, time: p.created_at, color: PAY_COLOR[p.status] ?? '#94a3b8' }); }); (recent.users ?? []).slice(0, 3).forEach(u => { evs.push({ title: 'ثبت‌نام کاربر جدید', sub: u.name || u.mobile, time: u.created_at, color: '#8b5cf6' }); }); return evs.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime()).slice(0, 8); }, [recent]); const apptRows = useMemo(() => (recent?.appointments ?? []).slice(0, 5).map(a => ({ title: a.user_name || a.user_mobile, sub: `دکتر ${a.doctor_name}`, meta: formatDateTime(a.slot_start), badgeLabel: APPT_LABEL[a.status] ?? a.status, badgeCls: APPT_CLS[a.status] ?? 'gray', initials: (a.user_name || a.user_mobile).slice(0, 2), hue: 222, })), [recent]); const payRows = useMemo(() => (recent?.payments ?? []).slice(0, 5).map(p => ({ title: p.user_name || p.user_mobile, sub: formatRial(p.amount), meta: formatDateTime(p.created_at), badgeLabel: PAY_LABEL[p.status] ?? p.status, badgeCls: PAY_CLS[p.status] ?? 'gray', initials: (p.user_name || p.user_mobile).slice(0, 2), hue: 162, })), [recent]); const userRows = useMemo(() => (recent?.users ?? []).slice(0, 5).map(u => ({ title: u.name || u.mobile, sub: u.name ? u.mobile : '', meta: formatDateTime(u.created_at), badgeLabel: 'فعال', badgeCls: 'green', initials: (u.name || u.mobile).slice(0, 2), hue: 256, })), [recent]); return (

داشبورد مدیریت

{today} · نمای کلی عملکرد مجموعه
{statsQ.isLoading ? Array.from({ length: 6 }).map((_, i) => ) : kpiCards.map(c => (
{c.label}
{c.value}
{c.hint}
)) }

وضعیت نوبت‌ها

{chartsQ.isLoading ? ( <>
{Array.from({ length: 5 }).map((_, i) => (
))}
) : ( <>
{donutData.map((d, i) => (
{d.label} {formatNumber(d.value)}
))}
)}

{chartMode === 'appts' ? 'نوبت‌ها' : 'درآمد'} — {preset === 'this_week' ? 'این هفته' : preset === 'this_month' ? 'این ماه' : '۳ ماه اخیر'}

{chartsQ.isLoading ? (
) : ( )}

پرتکرارترین تخصص‌ها

بر اساس تعداد نوبت
{chartsQ.isLoading ? (
) : !hbarsData.length ? (

داده‌ای موجود نیست

) : ( )}

فروش اشتراک‌ها

{preset === 'this_week' ? 'این هفته' : preset === 'this_month' ? 'این ماه' : '۳ ماه اخیر'}
{chartsQ.isLoading ? (
) : !(charts?.subscription_sales_by_plan ?? []).length ? (

فروشی ثبت نشده

) : ( {(charts?.subscription_sales_by_plan ?? []).map((row) => ( ))}
پنل تعداد درآمد
{row.plan === 'basic' ? 'پایه' : row.plan === 'professional' ? 'حرفه‌ای' : row.plan} {formatNumber(row.count)} {formatRial(row.total_rials)}
)}

دسترسی سریع

{quickActions.map(a => ( {a.label} ))}

آخرین رویدادها

{recentQ.isLoading ? (
{Array.from({ length: 5 }).map((_, i) => (
))}
) : !timelineEvents.length ? (

رویدادی ثبت نشده

) : (
{timelineEvents.map((e, i) => (
{e.title} {e.sub &&

{e.sub}

}
))}
)}
); } // ── Clinic Dashboard ────────────────────────────────────────────────────── interface ClinicDashboardData { clinic: { uuid: string; name: string; is_active: boolean; logo: string | null }; stats: { total_doctors: number; today_appointments: number; this_month_appointments: number; pending_invitations: number; sms_wallet_balance?: number; unique_patients_count?: number; revenue_period_rials?: number; }; today_appointments: ApptRow[]; doctors: { uuid: string; name: string; today_count: number }[]; } function ClinicDashboard() { const { context, dbUuid } = useAuthStore(); const [inviteOpen, setInviteOpen] = useState(false); const [preset, setPreset] = useState('this_month'); const range = getRange(preset); const q = useQuery({ queryKey: ['dashboard-clinic', preset], queryFn: () => api.get>(`/api/v1/dashboard/clinic?from=${range.from}&to=${range.to}`), staleTime: 60_000, }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const d = useMemo(() => (q.data?.data as any)?.data ?? q.data?.data, [q.data]); const today = new Date().toLocaleDateString('fa-IR', { year: 'numeric', month: 'long', day: 'numeric' }); const clinicUuid = d?.clinic.uuid ?? dbUuid ?? ''; if (q.isLoading) return ; const kpiCards = [ { label: 'پزشکان', value: formatNumber(d?.stats.total_doctors ?? 0), icon: HeartIcon, color: 'var(--success)', bg: 'var(--success-bg)' }, { label: 'نوبت‌های امروز', value: formatNumber(d?.stats.today_appointments ?? 0), icon: CalendarDaysIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' }, { label: 'نوبت‌های این ماه', value: formatNumber(d?.stats.this_month_appointments ?? 0), icon: ClockIcon, color: 'var(--info)', bg: 'var(--info-bg)' }, { label: 'دعوتنامه در انتظار', value: formatNumber(d?.stats.pending_invitations ?? 0), icon: BellAlertIcon, color: 'var(--danger)', bg: 'var(--danger-bg)' }, ...(d?.stats.sms_wallet_balance != null ? [{ label: 'موجودی پیامک', value: formatRial(d.stats.sms_wallet_balance), icon: CreditCardIcon, color: 'var(--primary)', bg: 'var(--primary-soft)' }] : []), ...(d?.stats.unique_patients_count != null ? [{ label: 'بیماران یکتا', value: formatNumber(d.stats.unique_patients_count), icon: UserGroupIcon, color: 'var(--violet)', bg: 'var(--violet-bg)' }] : []), ...(d?.stats.revenue_period_rials != null ? [{ label: 'درآمد دوره', value: formatRial(d.stats.revenue_period_rials), icon: CreditCardIcon, color: 'var(--success)', bg: 'var(--success-bg)' }] : []), ]; return (

داشبورد کلینیک

{today} · {d?.clinic.name ?? context?.name ?? ''}
{kpiCards.map(c => (
{c.label}
{c.value}
))}

نوبت‌های امروز

همه نوبت‌ها

پزشکان کلینیک

{clinicUuid && ( مدیریت )}
{!d?.doctors.length ? (

هنوز پزشکی دعوت نشده

) : (
{d.doctors.map((doc, i) => (
دکتر {doc.name}
{formatNumber(doc.today_count)} امروز
))}
)}
{inviteOpen && clinicUuid && ( setInviteOpen(false)} onInvited={() => q.refetch()} /> )}
); } // ── Doctor Clinic Invitations Card ─────────────────────────────────────── interface ClinicInvitation { uuid: string; invited_specialty: string | null; invited_at: number; expires_at: number; clinic: { uuid: string; name: string; logo: string | null }; } function DoctorClinicInvitationsCard() { const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['doctor-my-invitations'], queryFn: () => api.get<{ data: { data: ClinicInvitation[] } }>('/api/v1/doctor/invitations'), staleTime: 30_000, }); const respondMut = useMutation({ mutationFn: ({ uuid, action }: { uuid: string; action: 'accept' | 'reject' }) => api.post(`/api/v1/doctor/invitation/${uuid}/respond`, { action }), onSuccess: (_res, { action }) => { toast.success(action === 'accept' ? 'دعوتنامه پذیرفته شد' : 'دعوتنامه رد شد'); qc.invalidateQueries({ queryKey: ['doctor-my-invitations'] }); qc.invalidateQueries({ queryKey: ['dashboard-doctor'] }); }, onError: (err: Error) => toast.error(err.message), }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const invitations: ClinicInvitation[] = (data?.data as any) ?? []; if (!isLoading && invitations.length === 0) return null; return (

دعوتنامه‌های کلینیک

{invitations.length} در انتظار
{isLoading ? (
{[1, 2].map(i => (
))}
) : (
{invitations.map((inv) => (
{inv.clinic.logo ? ( ) : (
{(inv.clinic.name ?? '?')[0]}
)}
{inv.clinic.name} {inv.invited_specialty && {inv.invited_specialty}}
))}
)}
); } // ── Doctor Dashboard ────────────────────────────────────────────────────── interface DoctorDashboardData { doctor: { uuid: string; name: string; degree: string | null }; stats: { today_appointments: number; tomorrow_appointments: number; this_month_appointments: number; avg_rating: number | null; total_ratings: number; unique_patients_count?: number; revenue_period_rials?: number; }; today_appointments: ApptRow[]; clinics: { uuid: string; name: string; logo: string | null }[]; } function DoctorDashboard() { const { context } = useAuthStore(); const [preset, setPreset] = useState('this_month'); const range = getRange(preset); const q = useQuery({ queryKey: ['dashboard-doctor', preset], queryFn: () => api.get>(`/api/v1/dashboard/doctor?from=${range.from}&to=${range.to}`), staleTime: 60_000, }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const d = useMemo(() => (q.data?.data as any)?.data ?? q.data?.data, [q.data]); const today = new Date().toLocaleDateString('fa-IR', { year: 'numeric', month: 'long', day: 'numeric' }); if (q.isLoading) return ; const kpiCards = [ { label: 'نوبت‌های امروز', value: formatNumber(d?.stats.today_appointments ?? 0), icon: CalendarDaysIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' }, { label: 'نوبت‌های فردا', value: formatNumber(d?.stats.tomorrow_appointments ?? 0), icon: ClockIcon, color: 'var(--info)', bg: 'var(--info-bg)' }, { label: 'نوبت‌های این ماه', value: formatNumber(d?.stats.this_month_appointments ?? 0), icon: CalendarDaysIcon, color: 'var(--success)', bg: 'var(--success-bg)' }, { label: 'میانگین امتیاز', value: d?.stats.avg_rating != null ? String(d.stats.avg_rating) : '—', icon: StarIcon, color: 'var(--primary)', bg: 'var(--primary-soft)' }, ...(d?.stats.unique_patients_count != null ? [{ label: 'بیماران یکتا', value: formatNumber(d.stats.unique_patients_count), icon: UserGroupIcon, color: 'var(--violet)', bg: 'var(--violet-bg)' }] : []), ...(d?.stats.revenue_period_rials != null ? [{ label: 'درآمد دوره', value: formatRial(d.stats.revenue_period_rials), icon: CreditCardIcon, color: 'var(--success)', bg: 'var(--success-bg)' }] : []), ]; return (

داشبورد پزشک

{today} · دکتر {d?.doctor.name ?? context?.name ?? ''}
{kpiCards.map(c => (
{c.label}
{c.value}
{c.label === 'میانگین امتیاز' && d?.stats.total_ratings ? (
{formatNumber(d.stats.total_ratings)} نظر
) : null}
))}

نوبت‌های امروز

همه نوبت‌ها

کلینیک‌های من

{!d?.clinics.length ? (

عضو کلینیکی نیستید

) : (
{d.clinics.map((c, i) => (
{c.name}
فعال
))}
)}
); } // ── Secretary Dashboard ─────────────────────────────────────────────────── interface SecretaryDashboardData { doctor: { uuid: string; name: string; degree: string | null }; permissions: Record; stats: { today_appointments: number; tomorrow_appointments: number }; today_appointments: ApptRow[]; } function SecretaryDashboard() { const q = useQuery({ queryKey: ['dashboard-secretary'], queryFn: () => api.get>('/api/v1/dashboard/secretary'), staleTime: 60_000, }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const d = useMemo(() => (q.data?.data as any)?.data ?? q.data?.data, [q.data]); const today = new Date().toLocaleDateString('fa-IR', { year: 'numeric', month: 'long', day: 'numeric' }); if (q.isLoading) return ; const kpiCards = [ { label: 'نوبت‌های امروز', value: formatNumber(d?.stats.today_appointments ?? 0), icon: CalendarDaysIcon, color: 'var(--warning)', bg: 'var(--warning-bg)' }, { label: 'نوبت‌های فردا', value: formatNumber(d?.stats.tomorrow_appointments ?? 0), icon: ClockIcon, color: 'var(--info)', bg: 'var(--info-bg)' }, ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any const canViewAppts = (d?.permissions as any)?.resources?.appointments?.view ?? false; return (

داشبورد منشی

{today} · منشی دکتر {d?.doctor.name ?? ''}
دکتر {d?.doctor.name ?? '—'}
{d?.doctor.degree &&
{d.doctor.degree}
}
{canViewAppts ? 'دسترسی نوبت‌ها: فعال' : 'دسترسی نوبت‌ها: غیرفعال'}
{kpiCards.map(c => (
{c.label}
{c.value}
))}
{canViewAppts && (

نوبت‌های امروز

همه نوبت‌ها
)} {!canViewAppts && (

دسترسی مشاهده نوبت‌ها برای این منشی فعال نیست.

)}
); } // ── Main Dispatcher ─────────────────────────────────────────────────────── export default function DashboardPage() { const primaryRole = useAuthStore(s => s.primaryRole); if (!primaryRole) return ; if (primaryRole === 'admin') return ; if (primaryRole === 'clinic') return ; if (primaryRole === 'doctor') return ; if (primaryRole === 'secretary') return ; return ; }