import Link from "next/link"; import { addMonths, getYear, getMonth, format } from "date-fns"; import { de } from "date-fns/locale"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { prisma } from "@/lib/prisma"; import { formatUtcDateKey, getWorkingDaysOfMonth, getTargetMonthData, } from "@/lib/date-utils"; import { loadNotdienstDays } from "@/lib/notdienst-planning"; import { getNotdienstEmailStatus } from "@/lib/notdienst-notifications"; import { EmailStatusCard } from "./_components/email-status-card"; import { PlanView } from "./plan-view"; import { SettingsModal } from "./_components/settings-modal"; import { requireAlarmActor } from "@/lib/alarms/auth"; import { todayKey } from "@/lib/alarms/format"; export const metadata = { title: "Plan-Generierung · Kita-Planer" }; export default async function PlanungsZentralePage({ searchParams, }: { searchParams: Promise<{ year?: string; month?: string }>; }) { const actor = await requireAlarmActor(); const kitaId = actor.kitaId; const resolvedSearchParams = await searchParams; const now = new Date(); const defaultTarget = getTargetMonthData(now); const targetYear = resolvedSearchParams.year ? parseInt(resolvedSearchParams.year, 10) : defaultTarget.targetYear; const targetMonth = resolvedSearchParams.month ? parseInt(resolvedSearchParams.month, 10) : defaultTarget.targetMonth; const monthDate = new Date(targetYear, targetMonth - 1, 1); const monthName = format(monthDate, "MMMM", { locale: de }); const monthStart = new Date(Date.UTC(targetYear, targetMonth - 1, 1)); const monthEnd = new Date(Date.UTC(targetYear, targetMonth, 1)); const [plan, families, rawAvailabilities, workingDays, kita] = await Promise.all([ prisma.notdienstPlan.findUnique({ where: { kitaId_year_month: { kitaId, year: targetYear, month: targetMonth }, }, select: { id: true, status: true, publishedAt: true, assignments: { select: { id: true, childId: true, date: true, createdAt: true, alarms: { where: { activeAssignmentId: { not: null } }, select: { id: true }, }, }, }, }, }), prisma.family.findMany({ where: { kitaId, children: { some: { active: true } }, }, select: { id: true, name: true, children: { where: { active: true }, select: { id: true }, }, }, orderBy: { name: "asc" }, }), prisma.notdienstAvailability.findMany({ where: { kitaId, child: { kitaId, active: true }, date: { gte: monthStart, lt: monthEnd, }, }, select: { date: true, child: { select: { familyId: true, }, }, }, orderBy: { date: "asc" }, }), loadNotdienstDays(prisma, kitaId, targetYear, targetMonth), prisma.kita.findUniqueOrThrow({ where: { id: kitaId }, select: { notdienstMinPerChildPerMonth: true, notdienstReminderDaysBefore: true, }, }), ]); // Wir laden auch alle aktiven Kinder (inkl. Eltern-Namen) für das manuelle Dropdown. const allChildrenRaw = await prisma.child.findMany({ where: { kitaId, active: true }, select: { id: true, firstName: true, lastName: true, familyId: true, family: { select: { name: true, users: { select: { firstName: true, lastName: true, }, }, }, }, }, orderBy: { lastName: "asc" }, }); const allChildren = allChildrenRaw.map((c) => { const parentName = c.family.users .map((parent) => `${parent.firstName} ${parent.lastName}`) .join(", ") || c.family.name; return { id: c.id, name: `${c.firstName} ${c.lastName}`, parentName, familyId: c.familyId, }; }); const validDateKeys = new Set(workingDays.map(formatUtcDateKey)); const availabilities = rawAvailabilities.filter((a) => validDateKeys.has(formatUtcDateKey(a.date)), ); const closureCount = getWorkingDaysOfMonth(targetYear, targetMonth).length - workingDays.length; const availabilityDatesByFamily = new Map>(); availabilities.forEach((availability) => { const familyId = availability.child.familyId; const dates = availabilityDatesByFamily.get(familyId) ?? new Set(); dates.add(availability.date.toISOString()); availabilityDatesByFamily.set(familyId, dates); }); const familyStatuses = families.map((family) => { const dates = Array.from(availabilityDatesByFamily.get(family.id) ?? []); return { id: family.id, name: family.name, selectedDays: dates.map((date) => ({ iso: date, label: new Intl.DateTimeFormat("de-DE", { weekday: "short", day: "2-digit", month: "2-digit", }).format(new Date(date)), })), childCount: family.children.length, requiredDays: family.children.length * kita.notdienstMinPerChildPerMonth, }; }); const respondedFamilies = familyStatuses.filter( (family) => family.selectedDays.length >= family.requiredDays, ).length; const openFamilies = Math.max(familyStatuses.length - respondedFamilies, 0); const totalAvailabilitySlots = availabilities.length; const requiredDays = workingDays.length; const coverageFactor = requiredDays > 0 ? totalAvailabilitySlots / requiredDays : 0; // Wir mappen die Assignments auf die Werktage // Keep obsolete assignments visible so the coordinator can remove them. const displayedDays = new Map( workingDays.map((day) => [formatUtcDateKey(day), day]), ); for (const assignment of plan?.assignments ?? []) { displayedDays.set(formatUtcDateKey(assignment.date), assignment.date); } const activeChildIds = new Set(allChildren.map((child) => child.id)); const daysWithAssignments = Array.from(displayedDays.values()) .sort((a, b) => a.getTime() - b.getTime()) .map((day) => { const assignment = plan?.assignments.find( (a) => a.date.getTime() === day.getTime(), ); const isOriginallyUnassigned = !assignment || (plan?.publishedAt && assignment.createdAt > plan.publishedAt); const isInvalid = !validDateKeys.has(formatUtcDateKey(day)) || (!!assignment && !activeChildIds.has(assignment.childId)); return { date: day.toISOString(), isInvalid, assignmentId: assignment?.id || null, childId: assignment?.childId || null, isOriginallyUnassigned: !!isOriginallyUnassigned, activeAlarmId: assignment?.alarms[0]?.id ?? null, canAlarm: !!assignment && !isInvalid && plan?.status === "PUBLISHED" && day.toISOString().slice(0, 10) >= todayKey(), }; }); const mailStatus = await getNotdienstEmailStatus( kitaId, targetYear, targetMonth, ); const target = getTargetMonthData(now); const remindersOpen = !target.isLocked && target.targetYear === targetYear && target.targetMonth === targetMonth && plan?.status !== "PUBLISHED"; return (

NOTDIENST · PLANUNG

Notdienst{" "} zuweisen

Koordiniere und generiere den Notdienst-Plan.

{monthName} {targetYear}
= 32} />
); }