Files
clinicpro/assets/admin/pages/DashboardPage.tsx
T
hamed 147a2a894e feat: implement admin API for user and representation management
- Updated UsersPage to fetch users from the new admin endpoint.
- Enhanced user data structure to include 'name' and modified rendering logic.
- Added RepresentationDetailPage for detailed representation management.
- Created AdminApiController to handle user and representation CRUD operations.
- Implemented pagination and search functionality for users and representations.
- Updated user and representation data models to reflect new API structure.
2026-06-09 23:41:44 +03:30

179 lines
5.7 KiB
TypeScript

import React from 'react';
import { useQuery } from '@tanstack/react-query';
import {
UserGroupIcon,
HeartIcon,
BuildingOffice2Icon,
CalendarDaysIcon,
CreditCardIcon,
ChatBubbleLeftEllipsisIcon,
BanknotesIcon,
ArrowPathIcon,
} from '@heroicons/react/24/outline';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import { formatNumber, formatRial } from '../lib/utils';
interface DashboardStats {
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;
}
interface StatCardProps {
label: string;
value: string | number;
sub?: string;
icon: React.ElementType;
iconBg: string;
iconColor: string;
loading?: boolean;
}
function StatCard({ label, value, sub, icon: Icon, iconBg, iconColor, loading }: StatCardProps) {
return (
<div className="bg-white rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,.08)] p-6">
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<p className="text-sm text-gray-500 mb-1">{label}</p>
{loading ? (
<div className="h-8 w-24 bg-gray-100 rounded animate-pulse mt-1" />
) : (
<p className="text-2xl font-bold text-gray-900 truncate">{value}</p>
)}
{sub && !loading && (
<p className="text-xs text-gray-400 mt-1.5">{sub}</p>
)}
{sub && loading && (
<div className="h-3 w-32 bg-gray-100 rounded animate-pulse mt-2" />
)}
</div>
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 mr-3 ${iconBg}`}>
<Icon className={`w-6 h-6 ${iconColor}`} />
</div>
</div>
</div>
);
}
export default function DashboardPage() {
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ['dashboard-stats'],
queryFn: () => api.get<ApiResponse<DashboardStats>>('/api/v1/admin/dashboard/stats'),
staleTime: 60_000,
});
const stats: DashboardStats | undefined = (data?.data as any)?.data ?? data?.data;
const fn = (n: number | undefined) => (n !== undefined ? formatNumber(n) : '—');
const cards: StatCardProps[] = [
{
label: 'کل کاربران',
value: fn(stats?.total_users),
icon: UserGroupIcon,
iconBg: 'bg-violet-100',
iconColor: 'text-violet-600',
},
{
label: 'پزشکان فعال',
value: fn(stats?.active_doctors),
sub: stats ? `از ${formatNumber(stats.total_doctors)} پزشک` : undefined,
icon: HeartIcon,
iconBg: 'bg-emerald-100',
iconColor: 'text-emerald-600',
},
{
label: 'کلینیک‌ها',
value: fn(stats?.total_clinics),
icon: BuildingOffice2Icon,
iconBg: 'bg-blue-100',
iconColor: 'text-blue-600',
},
{
label: 'نوبت‌های امروز',
value: fn(stats?.today_appointments),
sub: stats ? `مجموع: ${formatNumber(stats.total_appointments)} نوبت` : undefined,
icon: CalendarDaysIcon,
iconBg: 'bg-orange-100',
iconColor: 'text-orange-600',
},
{
label: 'درآمد امروز',
value: stats?.today_payments_amount !== undefined ? formatRial(stats.today_payments_amount) : '—',
sub: stats ? `${formatNumber(stats.today_payments_count)} تراکنش` : undefined,
icon: CreditCardIcon,
iconBg: 'bg-pink-100',
iconColor: 'text-pink-600',
},
{
label: 'کل درآمد',
value: stats?.total_payments_amount !== undefined ? formatRial(stats.total_payments_amount) : '—',
icon: CreditCardIcon,
iconBg: 'bg-indigo-100',
iconColor: 'text-indigo-600',
},
{
label: 'نظرات در انتظار',
value: fn(stats?.pending_comments),
icon: ChatBubbleLeftEllipsisIcon,
iconBg: 'bg-yellow-100',
iconColor: 'text-yellow-600',
},
{
label: 'درخواست‌های تسویه',
value: fn(stats?.pending_settlements),
icon: BanknotesIcon,
iconBg: 'bg-teal-100',
iconColor: 'text-teal-600',
},
];
return (
<div>
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">داشبورد</h1>
<p className="text-sm text-gray-500 mt-1">خلاصه وضعیت سیستم</p>
</div>
{isError && (
<button
onClick={() => refetch()}
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-primary-600 transition-colors"
>
<ArrowPathIcon className="w-4 h-4" />
تلاش مجدد
</button>
)}
</div>
{isError && (
<div className="mb-4 bg-red-50 border border-red-200 rounded-xl px-4 py-3 text-sm text-red-600">
خطا در دریافت اطلاعات اطلاعات نمایش داده شده ممکن است به‌روز نباشند.
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
{cards.map((card) => (
<StatCard key={card.label} {...card} loading={isLoading} />
))}
</div>
<div className="mt-6 bg-white rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,.08)] p-6">
<h3 className="text-base font-semibold text-gray-800 mb-4">فعالیت‌های اخیر</h3>
<div className="text-center py-10 text-gray-400 text-sm">
در حال توسعه...
</div>
</div>
</div>
);
}