"use server"; import { createElement } from "react"; import { revalidatePath } from "next/cache"; import { NotdienstPlanStatus, UserRole } from "@prisma/client"; import { format } from "date-fns"; import { de } from "date-fns/locale"; import { requireRole } from "@/lib/auth-utils"; import { prisma } from "@/lib/prisma"; import { getTargetMonthData, getWorkingDaysOfMonth } from "@/lib/date-utils"; import { getAppEmailConfigError, sendAppEmail } from "@/lib/mail"; import { NotdienstAvailabilityReminderEmail } from "@/emails/NotdienstAvailabilityReminderEmail"; // ===================================================================== // /dashboard/notdienst/plan · Server Actions // ===================================================================== export async function generatePlanAction(year?: number, month?: number) { const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); const kitaId = session.user.kitaId!; let targetYear: number; let targetMonth: number; if (year !== undefined && month !== undefined) { targetYear = year; targetMonth = month; } else { const data = getTargetMonthData(); targetYear = data.targetYear; targetMonth = data.targetMonth; } // Prüfen, ob schon ein Plan existiert const existingPlan = await prisma.notdienstPlan.findUnique({ where: { kitaId_year_month: { kitaId, year: targetYear, month: targetMonth }, }, }); if (existingPlan) { return { error: "Ein Plan für diesen Monat existiert bereits." }; } // 1. Alle Verfügbarkeiten der Eltern für diesen Monat holen const availabilities = await prisma.notdienstAvailability.findMany({ where: { kitaId, date: { gte: new Date(targetYear, targetMonth - 1, 1), lt: new Date(targetYear, targetMonth, 1), }, }, include: { child: { select: { familyId: true } } }, }); // 2. Werktage holen const workingDays = getWorkingDaysOfMonth(targetYear, targetMonth); // 3. Algorithmus: Wir verteilen die Slots // Counter für "Gerechtigkeit": wie oft wurde diese Familie schon eingeteilt? const familyUsageCount: Record = {}; const assignmentsToCreate: { childId: string; date: Date }[] = []; for (const day of workingDays) { // Alle Verfügbarkeiten für diesen spezifischen Tag const availForDay = availabilities.filter( (a) => a.date.getTime() === day.getTime(), ); if (availForDay.length === 0) { // Niemand verfügbar -> Bleibt leer ("Unbesetzt") continue; } // Finde die Familie, die am wenigsten oft dran war // Zufall einbauen bei Gleichstand const sorted = availForDay.sort((a, b) => { const countA = familyUsageCount[a.child.familyId] || 0; const countB = familyUsageCount[b.child.familyId] || 0; if (countA === countB) { return Math.random() - 0.5; // Zufallsmischung } return countA - countB; }); const selected = sorted[0]; assignmentsToCreate.push({ childId: selected.childId, date: day, }); // Usage-Counter erhöhen familyUsageCount[selected.child.familyId] = (familyUsageCount[selected.child.familyId] || 0) + 1; } // 4. In der DB speichern try { await prisma.$transaction(async (tx) => { const plan = await tx.notdienstPlan.create({ data: { kitaId, year: targetYear, month: targetMonth, status: NotdienstPlanStatus.DRAFT, createdById: session.user.id, }, }); if (assignmentsToCreate.length > 0) { await tx.notdienstAssignment.createMany({ data: assignmentsToCreate.map((a) => ({ kitaId, planId: plan.id, childId: a.childId, date: a.date, })), }); } }); revalidatePath("/dashboard/notdienst/plan"); return { success: true }; } catch (error) { console.error(error); return { error: "Fehler bei der Generierung des Plans." }; } } export async function publishPlanAction(planId: string) { const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); const kitaId = session.user.kitaId!; await prisma.notdienstPlan.update({ where: { id: planId, kitaId }, data: { status: NotdienstPlanStatus.PUBLISHED, publishedAt: new Date(), }, }); revalidatePath("/dashboard/notdienst/plan"); revalidatePath("/dashboard/notdienst"); return { success: true }; } export async function updateAssignmentAction( planId: string, date: string, newChildId: string | null, ) { const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); const kitaId = session.user.kitaId!; const parsedDate = new Date(date); try { await prisma.$transaction(async (tx) => { // Prüfen, ob Plan noch DRAFT ist const plan = await tx.notdienstPlan.findUnique({ where: { id: planId, kitaId }, }); if (!plan || plan.status !== NotdienstPlanStatus.DRAFT) { throw new Error("Plan kann nicht mehr bearbeitet werden."); } // Altes Assignment für diesen Tag löschen await tx.notdienstAssignment.deleteMany({ where: { planId, date: parsedDate }, }); // Neues anlegen, falls ausgewählt if (newChildId) { const child = await tx.child.findFirst({ where: { id: newChildId, kitaId, active: true, }, select: { id: true }, }); if (!child) { throw new Error("Kind gehört nicht zu dieser Kita."); } await tx.notdienstAssignment.create({ data: { kitaId, planId, childId: newChildId, date: parsedDate, }, }); } }); revalidatePath("/dashboard/notdienst/plan"); return { success: true }; } catch (error) { return { error: error instanceof Error ? error.message : "Update fehlgeschlagen.", }; } } export async function updateKitaNotdienstSettingsAction( minPerChild: number, reminderDays: number, ) { const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); const kitaId = session.user.kitaId; if (!kitaId) { return { error: "Kein Mandant zugeordnet." }; } try { await prisma.kita.update({ where: { id: kitaId }, data: { notdienstMinPerChildPerMonth: minPerChild, notdienstReminderDaysBefore: reminderDays, }, }); revalidatePath("/dashboard/notdienst/plan"); return { success: true }; } catch (error) { console.error(error); return { error: "Fehler beim Speichern der Einstellungen." }; } } export async function sendNotdienstRemindersAction(year: number, month: number) { const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); const kitaId = session.user.kitaId; if (!kitaId) { return { error: "Kein Mandant zugeordnet." }; } const mailConfigError = getAppEmailConfigError(); if (mailConfigError) { return { error: mailConfigError }; } try { // 1. Get Kita Name const kita = await prisma.kita.findUnique({ where: { id: kitaId }, select: { name: true }, }); if (!kita) { return { error: "Kita nicht gefunden." }; } // 2. Fetch all families in this Kita with at least one active child and retrieve parents const families = await prisma.family.findMany({ where: { kitaId, children: { some: { active: true } }, }, include: { users: { where: { role: UserRole.ELTERN }, }, }, }); // 3. For target month, find availability entries const monthStart = new Date(year, month - 1, 1); const monthEnd = new Date(year, month, 1); const respondedFamilies = await prisma.notdienstAvailability.findMany({ where: { kitaId, date: { gte: monthStart, lt: monthEnd, }, }, select: { child: { select: { familyId: true, }, }, }, }); const respondedFamilyIds = new Set(respondedFamilies.map((a) => a.child.familyId)); // 4. Find open families (not responded yet) const openFamilies = families.filter((f) => !respondedFamilyIds.has(f.id)); if (openFamilies.length === 0) { return { success: true, count: 0 }; } // 5. Build emails list and send const monthDate = new Date(year, month - 1, 1); const monthLabel = format(monthDate, "MMMM yyyy", { locale: de }); const BASE_URL = process.env.NEXTAUTH_URL ?? process.env.AUTH_URL ?? "http://localhost:3000"; const link = `${BASE_URL}/dashboard/notdienst`; let sentCount = 0; for (const family of openFamilies) { for (const parent of family.users) { if (!parent.email) continue; const result = await sendAppEmail({ to: parent.email, subject: `Erinnerung: Bitte Verfügbarkeiten für den Notdienst ${monthLabel} eintragen`, react: createElement(NotdienstAvailabilityReminderEmail, { parentName: `${parent.firstName} ${parent.lastName}`, kitaName: kita.name, monthLabel, link, }), }); if (result.success) { sentCount++; } else { console.error(`E-Mail konnte nicht an ${parent.email} gesendet werden:`, result.error); } } } return { success: true, count: sentCount }; } catch (error) { console.error("Fehler beim Versenden der Erinnerungen:", error); return { error: "Fehler beim Versenden der Erinnerungen." }; } } export async function resetPlanAction(planId: string) { const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); const kitaId = session.user.kitaId; if (!kitaId) { return { error: "Kein Mandant zugeordnet." }; } try { await prisma.$transaction(async (tx) => { // Ensure the plan belongs to this kita and is draft const plan = await tx.notdienstPlan.findFirst({ where: { id: planId, kitaId }, }); if (!plan) { throw new Error("Plan nicht gefunden."); } if (plan.status !== NotdienstPlanStatus.DRAFT) { throw new Error("Nur Pläne im Entwurfsstatus können zurückgesetzt werden."); } // Delete all assignments await tx.notdienstAssignment.deleteMany({ where: { planId }, }); // Delete the plan itself await tx.notdienstPlan.delete({ where: { id: planId }, }); }); revalidatePath("/dashboard/notdienst/plan"); return { success: true }; } catch (error) { console.error("Fehler beim Zurücksetzen des Plans:", error); return { error: error instanceof Error ? error.message : "Fehler beim Zurücksetzen des Plans." }; } }