continued the kita-planer

This commit is contained in:
t.indorf
2026-05-08 14:32:14 +02:00
parent b686e714ff
commit 7aff691803
85 changed files with 9434 additions and 588 deletions
+247 -16
View File
@@ -1,6 +1,8 @@
import Link from "next/link";
import { Users, CalendarCheck2, ShieldCheck, AlertTriangle } from "lucide-react";
import { UserRole, NotdienstPlanStatus } from "@prisma/client";
import { format } from "date-fns";
import { de } from "date-fns/locale";
import { Users, CalendarCheck2, ShieldCheck, AlertTriangle, ClipboardList } from "lucide-react";
import { DutyAssignmentStatus, UserRole, NotdienstPlanStatus } from "@prisma/client";
import { requireKitaSession } from "@/lib/auth-utils";
import { prisma } from "@/lib/prisma";
@@ -13,7 +15,10 @@ import {
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { OnboardingDialog } from "@/components/OnboardingDialog";
import { AlertButton } from "./alert-button";
import { AbsenceCard } from "./absence-card";
import { NewsTicker } from "./news-ticker";
export const metadata = { title: "Übersicht · Kita-Planer" };
@@ -30,22 +35,166 @@ export default async function DashboardPage() {
},
});
// Schnelle Statistiken für die Übersicht
const [familyCount, childCount] = await Promise.all([
prisma.user.count({
where: { kitaId: session.user.kitaId, role: UserRole.ELTERN },
const today = new Date();
today.setHours(0, 0, 0, 0);
// Schnelle Statistiken und persönliche Dienste für die Übersicht.
const [
familyCount,
childCount,
upcomingDuties,
onboardingUser,
absenceChildren,
upcomingAbsences,
latestAnnouncements,
] = await Promise.all([
prisma.family.count({
where: { kitaId: session.user.kitaId },
}),
prisma.child.count({
where: { kitaId: session.user.kitaId },
}),
session.user.familyId
? prisma.dutyAssignment.findMany({
where: {
kitaId: session.user.kitaId,
familyId: session.user.familyId,
status: DutyAssignmentStatus.PLANNED,
endDate: { gte: today },
},
select: {
id: true,
startDate: true,
endDate: true,
dutyType: {
select: {
name: true,
},
},
},
orderBy: { startDate: "asc" },
take: 5,
})
: Promise.resolve([]),
prisma.user.findFirstOrThrow({
where: {
id: session.user.id,
kitaId: session.user.kitaId,
},
select: {
phone: true,
street: true,
postalCode: true,
city: true,
familyId: true,
role: true,
family: {
select: {
_count: {
select: {
children: true,
},
},
},
},
},
}),
session.user.familyId
? prisma.child.findMany({
where: {
kitaId: session.user.kitaId,
familyId: session.user.familyId,
active: true,
},
select: {
id: true,
firstName: true,
lastName: true,
},
orderBy: [{ lastName: "asc" }, { firstName: "asc" }],
})
: Promise.resolve([]),
session.user.familyId
? prisma.absence.findMany({
where: {
kitaId: session.user.kitaId,
endDate: { gte: today },
child: {
familyId: session.user.familyId,
},
},
select: {
id: true,
reason: true,
note: true,
startDate: true,
endDate: true,
child: {
select: {
firstName: true,
lastName: true,
},
},
},
orderBy: [{ startDate: "asc" }],
take: 6,
})
: Promise.resolve([]),
prisma.announcement.findMany({
where: { kitaId: session.user.kitaId },
select: {
id: true,
title: true,
content: true,
createdAt: true,
author: {
select: {
firstName: true,
lastName: true,
},
},
attachments: {
select: {
id: true,
fileName: true,
fileUrl: true,
fileType: true,
},
},
reads: {
where: { userId: session.user.id },
select: { id: true },
take: 1,
},
},
orderBy: { createdAt: "desc" },
take: 3,
}),
]);
const isAdmin =
session.user.role === UserRole.ADMIN ||
session.user.role === UserRole.SUPERADMIN;
const canReportAbsences = session.user.role !== UserRole.ERZIEHER;
return (
<div className="px-8 py-8">
<OnboardingDialog
user={{
phone: onboardingUser.phone,
street: onboardingUser.street,
postalCode: onboardingUser.postalCode,
city: onboardingUser.city,
familyId: onboardingUser.familyId,
role: onboardingUser.role,
}}
family={
onboardingUser.family
? { childrenCount: onboardingUser.family._count.children }
: null
}
/>
<div className="mb-8">
<h1 className="text-2xl font-semibold tracking-tight">
Willkommen, {session.user.name?.split(" ")[0] ?? "zusammen"}!
@@ -55,6 +204,25 @@ export default async function DashboardPage() {
</p>
</div>
<NewsTicker
items={latestAnnouncements.map((announcement) => ({
id: announcement.id,
title: announcement.title,
content: announcement.content,
createdAt: format(announcement.createdAt, "dd.MM.yyyy", {
locale: de,
}),
authorName: `${announcement.author.firstName} ${announcement.author.lastName}`,
isUnread: announcement.reads.length === 0,
attachments: announcement.attachments.map((attachment) => ({
id: attachment.id,
fileName: attachment.fileName,
fileUrl: attachment.fileUrl,
fileType: attachment.fileType,
})),
}))}
/>
{/* Heutiger Notdienst (Nur für Admins/Koordinatoren) */}
{isAdmin && (
<div className="mb-8">
@@ -62,10 +230,53 @@ export default async function DashboardPage() {
</div>
)}
{canReportAbsences && (
<AbsenceCard
today={format(today, "yyyy-MM-dd")}
childOptions={absenceChildren.map((child) => ({
id: child.id,
name: `${child.firstName} ${child.lastName}`,
}))}
absences={upcomingAbsences.map((absence) => ({
id: absence.id,
childName: `${absence.child.firstName} ${absence.child.lastName}`,
reason: absence.reason,
note: absence.note,
startDate: format(absence.startDate, "yyyy-MM-dd"),
endDate: format(absence.endDate, "yyyy-MM-dd"),
}))}
/>
)}
{upcomingDuties.length > 0 && (
<Card className="mb-8 border-emerald-200 bg-emerald-50/50">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg">
<ClipboardList className="h-5 w-5 text-emerald-700" />
Deine anstehenden Dienste
</CardTitle>
<CardDescription>
Die nächsten Einteilungen für deinen Haushalt.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{upcomingDuties.map((duty) => (
<div key={duty.id} className="rounded-md border bg-background p-3">
<p className="font-medium">{duty.dutyType.name}</p>
<p className="mt-1 text-sm text-muted-foreground">
{format(duty.startDate, "dd.MM.", { locale: de })} bis{" "}
{format(duty.endDate, "dd.MM.yyyy", { locale: de })}
</p>
</div>
))}
</CardContent>
</Card>
)}
{/* Statistik-Kacheln */}
<div className="mb-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<StatCard
label="Elternteile"
label="Familien"
value={familyCount}
description="registrierte Familien"
icon={<Users className="h-5 w-5 text-muted-foreground" />}
@@ -147,15 +358,38 @@ async function TodaysNotdienstCard({ kitaId }: { kitaId: string }) {
date: today,
plan: { status: NotdienstPlanStatus.PUBLISHED },
},
include: {
child: { include: { parentLinks: { include: { user: true } } } },
alerts: true, // Um zu sehen, ob schon ein Alarm ausgelöst wurde
select: {
id: true,
child: {
select: {
firstName: true,
lastName: true,
family: {
select: {
name: true,
users: {
select: {
firstName: true,
lastName: true,
},
},
},
},
},
},
alerts: {
select: {
status: true,
},
},
},
});
if (!assignment) return null;
const parent = assignment.child.parentLinks[0]?.user;
const parentNames = assignment.child.family.users
.map((parent) => `${parent.firstName} ${parent.lastName}`)
.join(", ");
const existingAlert = assignment.alerts[0]; // Kann PENDING, CONFIRMED oder CANCELLED sein
return (
@@ -172,7 +406,7 @@ async function TodaysNotdienstCard({ kitaId }: { kitaId: string }) {
<CardContent className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-medium text-base">
{parent ? `${parent.firstName} ${parent.lastName}` : "Unbekannt"}
{parentNames || assignment.child.family.name}
</p>
<p className="text-sm text-muted-foreground">
Kind: {assignment.child.firstName} {assignment.child.lastName}
@@ -198,10 +432,7 @@ async function TodaysNotdienstCard({ kitaId }: { kitaId: string }) {
: "Abgebrochen"}
</Badge>
) : (
<AlertButton
assignmentId={assignment.id}
parentUserId={parent?.id || ""}
/>
<AlertButton assignmentId={assignment.id} />
)}
</div>
</CardContent>