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.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
UserGroupIcon,
|
||||
HeartIcon,
|
||||
@@ -7,34 +8,55 @@ import {
|
||||
CreditCardIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
BanknotesIcon,
|
||||
ArrowTrendingUpIcon,
|
||||
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;
|
||||
trend?: string;
|
||||
trendUp?: boolean;
|
||||
sub?: string;
|
||||
icon: React.ElementType;
|
||||
iconBg: string;
|
||||
iconColor: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, trend, trendUp, icon: Icon, iconBg, iconColor }: StatCardProps) {
|
||||
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>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm text-gray-500 mb-1">{label}</p>
|
||||
<p className="text-3xl font-bold text-gray-900">{value}</p>
|
||||
{trend && (
|
||||
<div className={`flex items-center gap-1 mt-2 text-xs font-medium ${trendUp ? 'text-emerald-600' : 'text-red-500'}`}>
|
||||
<ArrowTrendingUpIcon className={`w-3.5 h-3.5 ${!trendUp && 'rotate-180'}`} />
|
||||
<span>{trend}</span>
|
||||
</div>
|
||||
{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 ${iconBg}`}>
|
||||
<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>
|
||||
@@ -42,79 +64,106 @@ function StatCard({ label, value, trend, trendUp, icon: Icon, iconBg, iconColor
|
||||
);
|
||||
}
|
||||
|
||||
const stats: StatCardProps[] = [
|
||||
{
|
||||
label: 'کل کاربران',
|
||||
value: '۱۲,۴۸۴',
|
||||
trend: '۸٪ نسبت به ماه قبل',
|
||||
trendUp: true,
|
||||
icon: UserGroupIcon,
|
||||
iconBg: 'bg-violet-100',
|
||||
iconColor: 'text-violet-600',
|
||||
},
|
||||
{
|
||||
label: 'پزشکان فعال',
|
||||
value: '۱,۲۸۴',
|
||||
trend: '۱۲٪ نسبت به ماه قبل',
|
||||
trendUp: true,
|
||||
icon: HeartIcon,
|
||||
iconBg: 'bg-emerald-100',
|
||||
iconColor: 'text-emerald-600',
|
||||
},
|
||||
{
|
||||
label: 'کلینیکها',
|
||||
value: '۳۲۱',
|
||||
trend: '۴٪ نسبت به ماه قبل',
|
||||
trendUp: false,
|
||||
icon: BuildingOffice2Icon,
|
||||
iconBg: 'bg-blue-100',
|
||||
iconColor: 'text-blue-600',
|
||||
},
|
||||
{
|
||||
label: 'نوبتهای امروز',
|
||||
value: '۱۴۸',
|
||||
trend: '۲۳٪ نسبت به دیروز',
|
||||
trendUp: true,
|
||||
icon: CalendarDaysIcon,
|
||||
iconBg: 'bg-orange-100',
|
||||
iconColor: 'text-orange-600',
|
||||
},
|
||||
{
|
||||
label: 'پرداختهای امروز',
|
||||
value: '۴۸,۲۰۰,۰۰۰',
|
||||
trend: '۱۵٪ نسبت به دیروز',
|
||||
trendUp: true,
|
||||
icon: CreditCardIcon,
|
||||
iconBg: 'bg-pink-100',
|
||||
iconColor: 'text-pink-600',
|
||||
},
|
||||
{
|
||||
label: 'نظرات در انتظار',
|
||||
value: '۱۲',
|
||||
icon: ChatBubbleLeftEllipsisIcon,
|
||||
iconBg: 'bg-yellow-100',
|
||||
iconColor: 'text-yellow-600',
|
||||
},
|
||||
{
|
||||
label: 'درخواست تسویه',
|
||||
value: '۵',
|
||||
icon: BanknotesIcon,
|
||||
iconBg: 'bg-teal-100',
|
||||
iconColor: 'text-teal-600',
|
||||
},
|
||||
];
|
||||
|
||||
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">
|
||||
<h1 className="text-2xl font-bold text-gray-900">داشبورد</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">خلاصه وضعیت سیستم</p>
|
||||
<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">
|
||||
{stats.map((stat) => (
|
||||
<StatCard key={stat.label} {...stat} />
|
||||
{cards.map((card) => (
|
||||
<StatCard key={card.label} {...card} loading={isLoading} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user