united-tattoo/components/admin/stats-dashboard.tsx

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>
)
}