"use server"; import { revalidatePath } from "next/cache"; import { UserRole } from "@prisma/client"; import { z } from "zod"; import { requireRole } from "@/lib/auth-utils"; import { formatUtcDateKey, getTargetMonthData } from "@/lib/date-utils"; import { serializableTransaction } from "@/lib/transaction"; import { UserFacingError, publicActionError } from "@/lib/action-error"; import { loadNotdienstDays } from "@/lib/notdienst-planning"; // ===================================================================== // /dashboard/notdienst · Server Actions // --------------------------------------------------------------------- // Speichert die gebuchten Termine der Eltern im Zielmonat. // Die Termine werden Round-Robin auf die Kinder aufgeteilt. // ===================================================================== const dateKeyPattern = /^\d{4}-\d{2}-\d{2}$/; const payloadSchema = z.object({ childrenIds: z.array(z.string().min(1).max(64)).min(1).max(20), dates: z.array( z.string().regex(dateKeyPattern).refine((value) => { const date = dateKeyToUtcDate(value); return Number.isFinite(date.getTime()) && value === date.toISOString().slice(0, 10); }), ).max(31), }); function dateKeyToUtcDate(dateKey: string) { const [year, month, day] = dateKey.split("-").map(Number); return new Date(Date.UTC(year, month - 1, day)); } export async function saveNotdienstAvailabilities(payloadRaw: { childrenIds: string[]; dates: string[]; }) { const session = await requireRole([ UserRole.ELTERN, UserRole.ADMIN, UserRole.KOORDINATOR, ]); const parsed = payloadSchema.safeParse(payloadRaw); if (!parsed.success) { return { error: "Ungültige Eingabedaten." }; } const { childrenIds } = parsed.data; const dates = Array.from(new Set(parsed.data.dates)).sort(); // ── 1. Sperrfrist prüfen ─────────────────────────────────────────── const { targetYear, targetMonth, isLocked } = getTargetMonthData(); if (isLocked) { return { error: "Die Planung für diesen Monat ist bereits abgeschlossen." }; } const kitaId = session.user.kitaId; if (!kitaId) { return { error: "Kein Mandant zugeordnet." }; } if (!session.user.familyId) { return { error: "Dein Account ist noch keinem Haushalt zugeordnet." }; } try { await serializableTransaction(async (tx) => { const [kita, children, workingDays] = await Promise.all([ tx.kita.findUniqueOrThrow({ where: { id: kitaId }, select: { notdienstMinPerChildPerMonth: true }, }), tx.child.findMany({ where: { kitaId, familyId: session.user.familyId!, active: true }, select: { id: true }, orderBy: { id: "asc" }, }), loadNotdienstDays(tx, kitaId, targetYear, targetMonth), ]); const suppliedIds = new Set(childrenIds); if (!children.length || suppliedIds.size !== childrenIds.length || suppliedIds.size !== children.length || !children.every((child) => suppliedIds.has(child.id))) { throw new UserFacingError("Bitte lade die Seite neu und wähle die Termine für alle aktiven Kinder deines Haushalts aus."); } const validDates = new Set(workingDays.map(formatUtcDateKey)); if (dates.some((date) => !validDates.has(date))) { throw new UserFacingError("Bitte wähle nur geöffnete Werktage im Zielmonat aus. Wochenenden und Schließtage sind nicht möglich."); } const requiredTotal = children.length * kita.notdienstMinPerChildPerMonth; if (dates.length < requiredTotal) { throw new UserFacingError(`Bitte wähle mindestens ${requiredTotal} Tage aus.`); } const activeIds = children.map((child) => child.id); await tx.notdienstAvailability.deleteMany({ where: { kitaId, child: { kitaId, familyId: session.user.familyId! }, date: { gte: new Date(Date.UTC(targetYear, targetMonth - 1, 1)), lt: new Date(Date.UTC(targetYear, targetMonth, 1)), }, }, }); await tx.notdienstAvailability.createMany({ data: dates.map((date, index) => ({ kitaId, childId: activeIds[index % activeIds.length], userId: session.user.id, date: dateKeyToUtcDate(date), })), }); }); revalidatePath("/dashboard/notdienst"); revalidatePath("/dashboard/notdienst/plan"); revalidatePath("/dashboard"); return { success: true, dates }; } catch (error) { if (!(error instanceof UserFacingError)) console.error("Fehler beim Speichern der Notdienste:", error); return { error: publicActionError(error, "Ein Fehler ist aufgetreten beim Speichern der Termine.") }; } }