Files
Calbook/app/(admin)/admin/uebersicht/page.tsx

157 lines
7.7 KiB
TypeScript

import Link from "next/link";
import { endOfMonth, startOfMonth, startOfDay, endOfDay, subWeeks, startOfWeek, addDays, format as fnsFormat } from "date-fns";
import { format } from "date-fns";
import { de } from "date-fns/locale";
import { BarChart3, Calendar, CheckCircle2, Clock, Users, AlertTriangle, ArrowRight } from "lucide-react";
import { prisma } from "@/lib/prisma";
import { LatestBookingsPanel } from "@/components/admin/latest-bookings-panel";
export const dynamic = "force-dynamic";
export default async function AdminOverviewPage() {
const now = new Date();
const eightWeeksAgo = subWeeks(now, 7);
const [
activeResources,
totalResources,
upcomingAppointments,
monthTotal,
monthCancelled,
monthNoShow,
openDeliveryIssues,
todayAppointments,
weeklyAppointments
] = await Promise.all([
prisma.calendarConn.count({ where: { user: { role: "STAFF", isActive: true } } }),
prisma.calendarConn.count({ where: { user: { role: "STAFF" } } }),
prisma.appointment.count({ where: { status: "CONFIRMED", startAt: { gte: now } } }),
prisma.appointment.count({ where: { status: "CONFIRMED", startAt: { gte: startOfMonth(now), lte: endOfMonth(now) } } }),
prisma.appointment.count({ where: { status: "CANCELLED", startAt: { gte: startOfMonth(now), lte: endOfMonth(now) } } }),
prisma.appointment.count({ where: { status: "CONFIRMED", noShowAt: { not: null }, startAt: { gte: startOfMonth(now), lte: endOfMonth(now) } } }),
prisma.deliveryIssue.count({ where: { resolvedAt: null } }),
prisma.appointment.findMany({
where: { status: "CONFIRMED", startAt: { gte: startOfDay(now), lte: endOfDay(now) } },
include: { staff: { select: { name: true } } },
orderBy: { startAt: "asc" }
}),
prisma.appointment.findMany({
where: { status: "CONFIRMED", startAt: { gte: eightWeeksAgo } },
select: { startAt: true }
})
]);
// Build weekly chart data
const weeks: { label: string; count: number }[] = [];
for (let i = 7; i >= 0; i--) {
const weekStart = startOfWeek(subWeeks(now, i), { weekStartsOn: 1 });
const weekEnd = addDays(weekStart, 6);
const count = weeklyAppointments.filter((a) => a.startAt >= weekStart && a.startAt <= weekEnd).length;
weeks.push({ label: format(weekStart, "dd.MM.", { locale: de }), count });
}
const maxCount = Math.max(1, ...weeks.map((w) => w.count));
return (
<div className="max-w-6xl mx-auto space-y-6">
<div>
<h1 className="text-3xl font-black tracking-tight text-slate-950">Dashboard</h1>
<p className="mt-1 text-sm font-medium text-slate-500">
{format(now, "EEEE, d. MMMM yyyy", { locale: de })}
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{[
{ icon: Calendar, iconBg: "bg-indigo-50", iconColor: "text-indigo-600", value: upcomingAppointments, label: "Offene Buchungen" },
{ icon: Users, iconBg: "bg-emerald-50", iconColor: "text-emerald-600", value: <>{activeResources}<span className="text-lg text-slate-400">/{totalResources}</span></>, label: "Aktive Kalender" },
{ icon: AlertTriangle, iconBg: "bg-amber-50", iconColor: "text-amber-600", value: openDeliveryIssues, label: "Zustellfehler" },
{ icon: CheckCircle2, iconBg: "bg-slate-900", iconColor: "text-white", value: <span className="text-emerald-600">Aktiv</span>, label: "System läuft" }
].map((stat, i) => (
<div key={i} className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<div className="mb-2 flex items-center gap-2">
<div className={`rounded-lg ${stat.iconBg} p-1.5`}>
<stat.icon className={`h-4 w-4 ${stat.iconColor}`} />
</div>
</div>
<div className="text-3xl font-black text-slate-900">{stat.value}</div>
<div className="text-xs font-medium text-slate-500">{stat.label}</div>
</div>
))}
</div>
{/* Chart + Today's appointments side by side */}
<div className="grid gap-6 lg:grid-cols-[1fr_380px]">
{/* Weekly chart */}
<div className="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<div className="flex items-center gap-2 border-b border-slate-100 px-5 py-3">
<BarChart3 className="h-4 w-4 text-slate-400" />
<h2 className="text-sm font-black text-slate-900">Buchungen</h2>
<span className="text-xs font-medium text-slate-400">letzte 8 Wochen</span>
</div>
<div className="p-5">
<div className="flex items-end gap-2 h-40">
{weeks.map((week, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-1.5 min-w-0">
<span className="text-[10px] font-bold text-slate-500">{week.count}</span>
<div
className="w-full rounded-t-md transition-all"
style={{ backgroundColor: "var(--accent)", height: `${Math.max(4, (week.count / maxCount) * 100)}%` }}
/>
<span className="text-[9px] font-medium text-slate-400 truncate w-full text-center">{week.label}</span>
</div>
))}
</div>
</div>
</div>
{/* Today's appointments */}
<div className="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<div className="flex items-center justify-between border-b border-slate-100 px-5 py-3">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-slate-400" />
<h2 className="text-sm font-black text-slate-900">Heutige Termine</h2>
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-bold text-slate-500">{todayAppointments.length}</span>
</div>
<Link href="/admin/termine" className="flex items-center gap-1 text-xs font-bold text-indigo-600 hover:text-indigo-700">
Alle <ArrowRight className="h-3 w-3" />
</Link>
</div>
{todayAppointments.length === 0 ? (
<div className="p-8 text-center text-sm text-slate-400">
<Clock className="mx-auto h-6 w-6 mb-2 opacity-30" />
Keine Termine für heute.
</div>
) : (
<div className="divide-y divide-slate-100 max-h-[300px] overflow-auto">
{todayAppointments.map((a) => (
<div key={a.id} className="flex items-center gap-3 px-5 py-3 text-sm">
<span className="shrink-0 text-sm font-black text-slate-900 w-12">{format(new Date(a.startAt), "HH:mm")}</span>
<div className="flex-1 min-w-0">
<p className="font-bold text-slate-900 truncate">{a.customerFirstName} {a.customerLastName}</p>
<p className="text-xs text-slate-500 truncate">{a.customerEmail}</p>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Latest bookings */}
<div>
<LatestBookingsPanel monthTotal={monthTotal} monthCancelled={monthCancelled} monthNoShow={monthNoShow} />
</div>
<div className="flex flex-wrap gap-3">
<Link href="/admin/kalender" className="inline-flex items-center gap-2 rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-bold text-white hover:bg-slate-800">
<Users className="h-4 w-4" /> Kalender verwalten
</Link>
<Link href="/admin/termine" className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-bold text-slate-700 hover:bg-slate-50">
<Calendar className="h-4 w-4" /> Termine anzeigen
</Link>
</div>
</div>
);
}