- 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.
179 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|