Files
kita-planer/src/app/admin/page.tsx
T
2026-05-16 12:02:59 +02:00

283 lines
9.4 KiB
TypeScript

import Link from "next/link";
import type { Metadata } from "next";
import {
AlertTriangle,
ArrowRight,
Building2,
CheckCircle2,
Clock,
Lock,
MailWarning,
Users,
} from "lucide-react";
import { UserRole } from "@prisma/client";
import { requireRole } from "@/lib/auth-utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
getGlobalAdminHealth,
getKitaOverviewRows,
roleOrder,
} from "./_lib/data";
import { KitaDeleteDialog } from "./_components/kita-delete-dialog";
export const metadata: Metadata = {
title: "Superadmin · Kita-Planer",
};
const numberFormat = new Intl.NumberFormat("de-DE");
function formatDate(date: Date | null) {
if (!date) return "Nie";
return new Intl.DateTimeFormat("de-DE", {
dateStyle: "medium",
timeStyle: "short",
}).format(date);
}
export default async function AdminPage() {
await requireRole([UserRole.SUPERADMIN]);
const [health, kitas] = await Promise.all([
getGlobalAdminHealth(),
getKitaOverviewRows(),
]);
const totalUsers = kitas.reduce((sum, kita) => sum + kita.counts.users, 0);
const totalChildren = kitas.reduce(
(sum, kita) => sum + kita.counts.activeChildren,
0,
);
const needsAttention =
health.lockedTenantUsers +
health.missingConsent +
health.openInvitations +
health.orphanTenantUsers;
return (
<div className="px-8 py-8">
<div className="mb-7 flex flex-wrap items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
Kita-Verwaltung
</h1>
<p className="mt-1 text-sm text-muted-foreground">
Systemweite Übersicht, read-only Diagnose und Support-Aktionen.
</p>
</div>
<Button asChild variant="outline">
<Link href="/admin/audit">
AuditLog ansehen
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</div>
<div className="mb-6 grid gap-4 md:grid-cols-4">
<MetricCard
icon={Building2}
label="Kitas"
value={health.kitas}
description="Mandanten im System"
/>
<MetricCard
icon={Users}
label="Benutzer"
value={totalUsers}
description={`${numberFormat.format(totalChildren)} aktive Kinder`}
/>
<MetricCard
icon={AlertTriangle}
label="Hinweise"
value={needsAttention}
description="Sperren, Einladungen, Consent"
attention={needsAttention > 0}
/>
<MetricCard
icon={Lock}
label="Gesperrt"
value={health.lockedTenantUsers}
description="Aktive Account-Sperren"
attention={health.lockedTenantUsers > 0}
/>
</div>
{health.orphanTenantUsers > 0 && (
<div className="mb-6 rounded-md border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-200">
<div className="flex items-center gap-2 font-medium">
<MailWarning className="h-4 w-4" />
{health.orphanTenantUsers} tenant-lose Nicht-Superadmin-Benutzer
</div>
<p className="mt-1 text-amber-800 dark:text-amber-300">
Diese Konten sind keiner Kita zugeordnet und sollten separat
bereinigt werden. V1 verwaltet nur tenant-gebundene Benutzer.
</p>
</div>
)}
<Card>
<CardHeader>
<CardTitle as="h2" className="text-xl">
Alle Kitas
</CardTitle>
<CardDescription>
Diagnose-Links sind read-only; Löschungen benötigen eine exakte
Namensbestätigung.
</CardDescription>
</CardHeader>
<CardContent>
{kitas.length === 0 ? (
<div className="rounded-md border border-dashed p-8 text-center text-sm text-muted-foreground">
Noch keine Kitas angelegt.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Kita</TableHead>
<TableHead>Status</TableHead>
<TableHead>Counts</TableHead>
<TableHead>Rollen</TableHead>
<TableHead>Letzter Login</TableHead>
<TableHead className="text-right">Aktionen</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{kitas.map((kita) => {
const warnings =
kita.counts.lockedUsers +
kita.counts.missingConsent +
kita.counts.openInvitations +
kita.counts.familiesWithoutUsers;
return (
<TableRow key={kita.id}>
<TableCell>
<div className="font-medium">{kita.name}</div>
<div className="text-xs text-muted-foreground">
{kita.slug}
</div>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1.5">
{warnings === 0 ? (
<Badge variant="success">
<CheckCircle2 className="mr-1 h-3 w-3" />
Unauffällig
</Badge>
) : (
<Badge variant="warning">
<AlertTriangle className="mr-1 h-3 w-3" />
{warnings} Hinweise
</Badge>
)}
{kita.counts.lockedUsers > 0 && (
<Badge variant="outline">
{kita.counts.lockedUsers} gesperrt
</Badge>
)}
{kita.counts.openInvitations > 0 && (
<Badge variant="outline">
{kita.counts.openInvitations} Einladungen
</Badge>
)}
</div>
</TableCell>
<TableCell>
<div className="grid min-w-48 grid-cols-2 gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span>{kita.counts.users} Benutzer</span>
<span>{kita.counts.families} Familien</span>
<span>{kita.counts.activeChildren} Kinder</span>
<span>{kita.counts.termine} Termine</span>
</div>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{roleOrder.map((role) =>
kita.roleCounts[role] > 0 ? (
<Badge key={role} variant="secondary">
{role}: {kita.roleCounts[role]}
</Badge>
) : null,
)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2 text-sm">
<Clock className="h-4 w-4 text-muted-foreground" />
{formatDate(kita.lastLoginAt)}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex flex-wrap justify-end gap-2">
<Button asChild variant="outline" size="sm">
<Link
href={`/admin/kitas/${kita.id}`}
prefetch={false}
>
Diagnose
</Link>
</Button>
<KitaDeleteDialog
kitaId={kita.id}
kitaName={kita.name}
counts={kita.counts}
/>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}
function MetricCard({
icon: Icon,
label,
value,
description,
attention = false,
}: {
icon: typeof Building2;
label: string;
value: number;
description: string;
attention?: boolean;
}) {
return (
<Card className={attention ? "border-amber-200 bg-amber-50/70" : ""}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardDescription>{label}</CardDescription>
<Icon className="h-4 w-4 text-muted-foreground" />
</div>
<CardTitle className="text-3xl">{numberFormat.format(value)}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-xs text-muted-foreground">{description}</p>
</CardContent>
</Card>
);
}