application [cat-admin-web] view page [pages/dashboard] development

This commit is contained in:
NVWA Code Agent
2025-12-11 16:45:14 +00:00
parent 66fd927eb5
commit 68f291d76c

View File

@@ -0,0 +1,250 @@
import { useEffect, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { entities } from "@/lib/nvwa";
interface CatInventoryStats {
totalCats: number;
availableCats: number;
gradeDistribution: { [key: string]: number };
}
interface ReservationStats {
totalReservations: number;
statusDistribution: { [key: string]: number };
recentReservations: number; // 最近7天
}
interface QueueStatusSummary {
queuing: number;
reserved: number;
completed: number;
cancelled: number;
}
export default function DashboardPage() {
const [catStats, setCatStats] = useState<CatInventoryStats>({
totalCats: 0,
availableCats: 0,
gradeDistribution: {}
});
const [reservationStats, setReservationStats] = useState<ReservationStats>({
totalReservations: 0,
statusDistribution: {},
recentReservations: 0
});
const [queueStats, setQueueStats] = useState<QueueStatusSummary>({
queuing: 0,
reserved: 0,
completed: 0,
cancelled: 0
});
const [loading, setLoading] = useState(true);
useEffect(() => {
loadDashboardData();
}, []);
const loadDashboardData = async () => {
try {
setLoading(true);
// 获取猫咪库存数据
const { data: cats } = await entities.from("cat").select("id, is_available, grade");
if (cats) {
const totalCats = cats.length;
const availableCats = cats.filter(cat => cat.is_available).length;
const gradeDistribution = cats.reduce((acc, cat) => {
acc[cat.grade] = (acc[cat.grade] || 0) + 1;
return acc;
}, {} as { [key: string]: number });
setCatStats({ totalCats, availableCats, gradeDistribution });
}
// 获取预约数据
const { data: reservations } = await entities.from("reservation").select("id, status, created_at");
if (reservations) {
const totalReservations = reservations.length;
const statusDistribution = reservations.reduce((acc, res) => {
acc[res.status] = (acc[res.status] || 0) + 1;
return acc;
}, {} as { [key: string]: number });
// 计算最近7天的预约
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
const recentReservations = reservations.filter(res =>
new Date(res.created_at) >= sevenDaysAgo
).length;
setReservationStats({ totalReservations, statusDistribution, recentReservations });
// 设置排队状态摘要
setQueueStats({
queuing: statusDistribution.queuing || 0,
reserved: statusDistribution.reserved || 0,
completed: statusDistribution.completed || 0,
cancelled: statusDistribution.cancelled || 0
});
}
} catch (error) {
console.error("Failed to load dashboard data:", error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">...</p>
</div>
</div>
);
}
return (
<div className="container mx-auto py-6 px-4">
<div className="mb-6">
<h1 className="text-3xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
{/* 猫咪库存概览 */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{catStats.totalCats}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{catStats.availableCats}</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{Object.entries(catStats.gradeDistribution).map(([grade, count]) => (
<div key={grade} className="flex items-center justify-between">
<Badge variant="outline" className="text-xs">
{grade}
</Badge>
<span className="text-sm font-medium">{count}</span>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{catStats.totalCats > 0 ? Math.round((catStats.availableCats / catStats.totalCats) * 100) : 0}%
</div>
<p className="text-xs text-muted-foreground"></p>
</CardContent>
</Card>
</div>
{/* 预约统计 */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 mb-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<span className="text-2xl font-bold">{reservationStats.totalReservations}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">7</span>
<span className="text-lg font-semibold text-blue-600">{reservationStats.recentReservations}</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{Object.entries(reservationStats.statusDistribution).map(([status, count]) => (
<div key={status} className="flex items-center justify-between">
<Badge variant={
status === 'completed' ? 'default' :
status === 'reserved' ? 'secondary' :
status === 'cancelled' ? 'destructive' :
'outline'
} className="text-xs">
{status === 'queuing' ? '排队中' :
status === 'reserved' ? '已预约' :
status === 'completed' ? '已完成' :
status === 'cancelled' ? '已取消' : status}
</Badge>
<span className="text-sm font-medium">{count}</span>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-orange-600"></span>
<span className="text-lg font-bold">{queueStats.queuing}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-blue-600"></span>
<span className="text-lg font-bold">{queueStats.reserved}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-green-600"></span>
<span className="text-lg font-bold">{queueStats.completed}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-red-600"></span>
<span className="text-lg font-bold">{queueStats.cancelled}</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}