155 lines
6.8 KiB
TypeScript
155 lines
6.8 KiB
TypeScript
import { createServiceClient } from "@/lib/supabase";
|
||
import { verifySessionToken } from "@/lib/admin-auth";
|
||
import { AdminNav } from "@/components/admin/AdminNav";
|
||
import { cookies } from "next/headers";
|
||
import { redirect } from "next/navigation";
|
||
import type { Metadata } from "next";
|
||
|
||
export const metadata: Metadata = { title: "Statistik – MBO Tech IT" };
|
||
export const dynamic = "force-dynamic";
|
||
|
||
function fmt(n: number) {
|
||
return n.toLocaleString("de-DE");
|
||
}
|
||
|
||
export default async function StatistikPage() {
|
||
const cookieStore = await cookies();
|
||
const token = cookieStore.get("admin_session")?.value;
|
||
if (!token) redirect("/admin/login");
|
||
const session = await verifySessionToken(token);
|
||
if (!session) redirect("/admin/login?session_expired=true");
|
||
|
||
const db = createServiceClient();
|
||
|
||
const { data: alleAnfragen } = await db
|
||
.from("anfragen")
|
||
.select("id, status, created_at, name, betreff");
|
||
|
||
const anfragen = alleAnfragen ?? [];
|
||
const gesamt = anfragen.length;
|
||
const offen = anfragen.filter((a) => a.status === "offen").length;
|
||
const inBearbeitung = anfragen.filter((a) => a.status === "in_bearbeitung").length;
|
||
const abgeschlossen = anfragen.filter((a) => a.status === "abgeschlossen").length;
|
||
|
||
const { data: recentRaw } = await db
|
||
.from("anfragen")
|
||
.select("id, created_at, name, betreff, status, email")
|
||
.order("created_at", { ascending: false })
|
||
.limit(20);
|
||
|
||
const recentAnfragen = recentRaw ?? [];
|
||
|
||
const heute = new Date();
|
||
const monatsStats: { monat: string; label: string; count: number }[] = [];
|
||
for (let i = 5; i >= 0; i--) {
|
||
const d = new Date(heute);
|
||
d.setMonth(d.getMonth() - i);
|
||
const monat = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||
const label = d.toLocaleDateString("de-DE", { month: "short", year: "2-digit" });
|
||
const count = anfragen.filter((a) => a.created_at.startsWith(monat)).length;
|
||
monatsStats.push({ monat, label, count });
|
||
}
|
||
const maxCount = Math.max(...monatsStats.map((m) => m.count), 1);
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#111925]">
|
||
<AdminNav />
|
||
|
||
<div className="max-w-7xl mx-auto px-4 py-8 space-y-8">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-white tracking-tight">Statistik</h1>
|
||
<p className="text-sm text-slate-500 mt-0.5">Anfragen-Übersicht</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||
{[
|
||
{ label: "Anfragen gesamt", wert: fmt(gesamt), farbe: "border-l-slate-400" },
|
||
{ label: "Offen", wert: fmt(offen), farbe: "border-l-amber-500" },
|
||
{ label: "In Bearbeitung", wert: fmt(inBearbeitung), farbe: "border-l-blue-500" },
|
||
{ label: "Abgeschlossen", wert: fmt(abgeschlossen), farbe: "border-l-green-500" },
|
||
].map((k) => (
|
||
<div key={k.label} className={`bg-[#18212f] border border-gray-800 border-l-4 ${k.farbe} p-4`}>
|
||
<p className="text-xs text-slate-500 uppercase tracking-wide font-medium mb-1">{k.label}</p>
|
||
<p className="text-2xl font-bold text-white font-mono">{k.wert}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="bg-[#18212f] border border-gray-800 p-5">
|
||
<h2 className="font-semibold text-white mb-5">Anfragen letzte 6 Monate</h2>
|
||
<div className="flex items-end gap-3 h-32">
|
||
{monatsStats.map((m) => (
|
||
<div key={m.monat} className="flex-1 flex flex-col items-center gap-1">
|
||
<span className="text-[10px] font-mono text-slate-500">
|
||
{m.count > 0 ? m.count : "–"}
|
||
</span>
|
||
<div className="w-full flex items-end" style={{ height: 80 }}>
|
||
<div
|
||
className="w-full bg-orange-500 rounded-sm transition-all"
|
||
style={{
|
||
height: `${Math.max(m.count > 0 ? 8 : 0, Math.round((m.count / maxCount) * 80))}px`,
|
||
}}
|
||
/>
|
||
</div>
|
||
<span className="text-[10px] text-slate-400 font-medium">{m.label}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-[#18212f] border border-gray-800">
|
||
<div className="p-5 border-b border-gray-800">
|
||
<h2 className="font-semibold text-white">Letzte 20 Anfragen</h2>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead className="border-b border-gray-800">
|
||
<tr className="text-xs text-slate-500 uppercase tracking-wide">
|
||
<th className="text-left py-3 px-5 font-medium">Datum</th>
|
||
<th className="text-left py-3 px-5 font-medium">Name</th>
|
||
<th className="text-left py-3 px-5 font-medium hidden sm:table-cell">Betreff</th>
|
||
<th className="text-left py-3 px-5 font-medium">Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-800/50">
|
||
{recentAnfragen.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={4} className="px-5 py-8 text-center text-slate-500">
|
||
Keine Anfragen vorhanden
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
recentAnfragen.map((a) => (
|
||
<tr key={a.id} className="hover:bg-[#111925] transition-colors">
|
||
<td className="px-5 py-3 text-slate-400 whitespace-nowrap">
|
||
{new Date(a.created_at).toLocaleDateString("de-DE")}
|
||
</td>
|
||
<td className="px-5 py-3 font-medium text-white">{a.name}</td>
|
||
<td className="px-5 py-3 text-slate-400 hidden sm:table-cell truncate max-w-xs">{a.betreff}</td>
|
||
<td className="px-5 py-3">
|
||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||
a.status === "offen"
|
||
? "bg-amber-500/10 text-amber-400 border border-amber-500/20"
|
||
: a.status === "in_bearbeitung"
|
||
? "bg-blue-500/10 text-blue-400 border border-blue-500/20"
|
||
: "bg-green-500/10 text-green-400 border border-green-500/20"
|
||
}`}>
|
||
{a.status === "offen"
|
||
? "Offen"
|
||
: a.status === "in_bearbeitung"
|
||
? "In Bearbeitung"
|
||
: "Abgeschlossen"}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|