376 lines
12 KiB
TypeScript
376 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Progress } from '@/components/ui/progress'
|
|
import {
|
|
Users,
|
|
Calendar,
|
|
DollarSign,
|
|
TrendingUp,
|
|
Clock,
|
|
CheckCircle,
|
|
XCircle,
|
|
AlertCircle,
|
|
Image,
|
|
Upload
|
|
} from 'lucide-react'
|
|
import {
|
|
BarChart,
|
|
Bar,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
LineChart,
|
|
Line,
|
|
PieChart,
|
|
Pie,
|
|
Cell
|
|
} from 'recharts'
|
|
|
|
interface DashboardStats {
|
|
artists: {
|
|
total: number
|
|
active: number
|
|
inactive: number
|
|
}
|
|
appointments: {
|
|
total: number
|
|
pending: number
|
|
confirmed: number
|
|
inProgress: number
|
|
completed: number
|
|
cancelled: number
|
|
thisMonth: number
|
|
lastMonth: number
|
|
revenue: number
|
|
}
|
|
portfolio: {
|
|
totalImages: number
|
|
recentUploads: number
|
|
}
|
|
files: {
|
|
totalUploads: number
|
|
totalSize: number
|
|
recentUploads: number
|
|
}
|
|
monthlyData: Array<{
|
|
month: string
|
|
appointments: number
|
|
revenue: number
|
|
}>
|
|
statusData: Array<{
|
|
name: string
|
|
value: number
|
|
color: string
|
|
}>
|
|
}
|
|
|
|
const COLORS = {
|
|
pending: '#f59e0b',
|
|
confirmed: '#3b82f6',
|
|
inProgress: '#10b981',
|
|
completed: '#6b7280',
|
|
cancelled: '#ef4444',
|
|
}
|
|
|
|
export function StatsDashboard() {
|
|
const { data: stats, isLoading } = useQuery({
|
|
queryKey: ['dashboard-stats'],
|
|
queryFn: async () => {
|
|
const response = await fetch('/api/admin/stats')
|
|
if (!response.ok) throw new Error('Failed to fetch stats')
|
|
return response.json() as Promise<DashboardStats>
|
|
},
|
|
refetchInterval: 30000, // Refresh every 30 seconds
|
|
})
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{Array.from({ length: 8 }).map((_, i) => (
|
|
<Card key={i}>
|
|
<CardHeader className="animate-pulse">
|
|
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
|
</CardHeader>
|
|
<CardContent className="animate-pulse">
|
|
<div className="h-8 bg-gray-200 rounded w-1/2"></div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!stats) {
|
|
return (
|
|
<div className="text-center py-8">
|
|
<AlertCircle className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
|
<p className="text-muted-foreground">Failed to load dashboard statistics</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const appointmentGrowth = stats.appointments.thisMonth > 0
|
|
? ((stats.appointments.thisMonth - stats.appointments.lastMonth) / stats.appointments.lastMonth) * 100
|
|
: 0
|
|
|
|
const activeArtistPercentage = stats.artists.total > 0
|
|
? (stats.artists.active / stats.artists.total) * 100
|
|
: 0
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Key Metrics */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Artists</CardTitle>
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.artists.total}</div>
|
|
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
|
|
<span>{stats.artists.active} active</span>
|
|
<Progress value={activeArtistPercentage} className="w-16 h-1" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Appointments</CardTitle>
|
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.appointments.total}</div>
|
|
<div className="flex items-center space-x-1 text-xs">
|
|
<TrendingUp className={`h-3 w-3 ${appointmentGrowth >= 0 ? 'text-green-500' : 'text-red-500'}`} />
|
|
<span className={appointmentGrowth >= 0 ? 'text-green-500' : 'text-red-500'}>
|
|
{appointmentGrowth >= 0 ? '+' : ''}{appointmentGrowth.toFixed(1)}%
|
|
</span>
|
|
<span className="text-muted-foreground">from last month</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Monthly Revenue</CardTitle>
|
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
${stats.appointments.revenue.toLocaleString()}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
From {stats.appointments.thisMonth} appointments this month
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Portfolio Images</CardTitle>
|
|
<Image className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.portfolio.totalImages}</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{stats.portfolio.recentUploads} uploaded this week
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Appointment Status Overview */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Pending</CardTitle>
|
|
<Clock className="h-4 w-4 text-yellow-500" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-yellow-600">
|
|
{stats.appointments.pending}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Confirmed</CardTitle>
|
|
<CheckCircle className="h-4 w-4 text-blue-500" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-blue-600">
|
|
{stats.appointments.confirmed}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">In Progress</CardTitle>
|
|
<AlertCircle className="h-4 w-4 text-green-500" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{stats.appointments.inProgress}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Completed</CardTitle>
|
|
<CheckCircle className="h-4 w-4 text-gray-500" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-gray-600">
|
|
{stats.appointments.completed}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Cancelled</CardTitle>
|
|
<XCircle className="h-4 w-4 text-red-500" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-red-600">
|
|
{stats.appointments.cancelled}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Charts */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Monthly Appointments Trend */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Monthly Appointments</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<LineChart data={stats.monthlyData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="month" />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="appointments"
|
|
stroke="#3b82f6"
|
|
strokeWidth={2}
|
|
dot={{ fill: '#3b82f6' }}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Appointment Status Distribution */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Appointment Status Distribution</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<PieChart>
|
|
<Pie
|
|
data={stats.statusData}
|
|
cx="50%"
|
|
cy="50%"
|
|
labelLine={false}
|
|
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
|
outerRadius={80}
|
|
fill="#8884d8"
|
|
dataKey="value"
|
|
>
|
|
{stats.statusData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Monthly Revenue Trend */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Monthly Revenue Trend</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={stats.monthlyData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="month" />
|
|
<YAxis />
|
|
<Tooltip
|
|
formatter={(value) => [`$${value}`, 'Revenue']}
|
|
/>
|
|
<Bar dataKey="revenue" fill="#10b981" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* File Storage Stats */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Files</CardTitle>
|
|
<Upload className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.files.totalUploads}</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{stats.files.recentUploads} uploaded this week
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Storage Used</CardTitle>
|
|
<Upload className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{(stats.files.totalSize / (1024 * 1024)).toFixed(1)} MB
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Across all uploads
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Active Artists</CardTitle>
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.artists.active}</div>
|
|
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
|
|
<span>of {stats.artists.total} total</span>
|
|
<Badge variant="secondary">
|
|
{activeArtistPercentage.toFixed(0)}%
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|