diff --git a/assets/admin/pages/DashboardPage.tsx b/assets/admin/pages/DashboardPage.tsx index 9ab1c5ac..c8aecfdf 100644 --- a/assets/admin/pages/DashboardPage.tsx +++ b/assets/admin/pages/DashboardPage.tsx @@ -259,10 +259,11 @@ interface AdminStats { pending_settlements: number; this_month_revenue: number; this_month_appointments: number; } interface AdminCharts { - appointments_30d: { date: string; count: number }[]; - revenue_30d: { date: string; amount: number }[]; + 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 }[]; @@ -270,11 +271,31 @@ interface AdminRecent { 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'], queryFn: () => api.get>('/api/v1/admin/dashboard/charts'), staleTime: 120_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 @@ -289,8 +310,8 @@ function AdminDashboard() { 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_30d?.map(d => d.count) ?? [], [charts]); - const revSeries = useMemo(() => charts?.revenue_30d?.map(d => d.amount) ?? [], [charts]); + 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', @@ -358,8 +379,12 @@ function AdminDashboard() {

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

{today} · نمای کلی عملکرد مجموعه
-
- +
+
+ + + +
-

{chartMode === 'appts' ? 'نوبت‌ها' : 'درآمد'} — ۳۰ روز اخیر

+

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

@@ -436,18 +461,50 @@ function AdminDashboard() {
-
-
-

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

- بر اساس تعداد نوبت +
+
+
+

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

+ بر اساس تعداد نوبت +
+ {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)}
+ )}
- {chartsQ.isLoading ? ( -
- ) : !hbarsData.length ? ( -

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

- ) : ( - - )}
@@ -510,7 +567,10 @@ function AdminDashboard() { 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 }; + 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 }[]; } @@ -518,9 +578,11 @@ interface ClinicDashboardData { 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'], - queryFn: () => api.get>('/api/v1/dashboard/clinic'), + queryKey: ['dashboard-clinic', preset], + queryFn: () => api.get>(`/api/v1/dashboard/clinic?from=${range.from}&to=${range.to}`), staleTime: 60_000, }); @@ -536,6 +598,9 @@ function ClinicDashboard() { { 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 ( @@ -545,7 +610,12 @@ function ClinicDashboard() {

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

{today} · {d?.clinic.name ?? context?.name ?? ''}
-
+
+
+ + + +
- +
+
+ + + +
+ +