feat: complete notdienst planning, quick wins and notifications

This commit is contained in:
t.indorf
2026-10-12 15:34:09 +02:00
parent b915857951
commit 00f602a7aa
50 changed files with 2443 additions and 513 deletions
+44 -72
View File
@@ -5,8 +5,10 @@ import { UserRole } from "@prisma/client";
import { z } from "zod";
import { requireRole } from "@/lib/auth-utils";
import { prisma } from "@/lib/prisma";
import { getTargetMonthData } from "@/lib/date-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
@@ -64,90 +66,60 @@ export async function saveNotdienstAvailabilities(payloadRaw: {
return { error: "Dein Account ist noch keinem Haushalt zugeordnet." };
}
// ── 2. Validierung (Soll-Tage) ─────────────────────────────────────
const kita = await prisma.kita.findUniqueOrThrow({
where: { id: kitaId },
select: { notdienstMinPerChildPerMonth: true },
});
const requiredTotal = childrenIds.length * kita.notdienstMinPerChildPerMonth;
if (dates.length < requiredTotal) {
return {
error: `Bitte wähle mindestens ${requiredTotal} Tage aus.`,
};
}
// Sicherstellen, dass alle Kinder zum Haushalt des angemeldeten Users gehören.
const validChildren = await prisma.child.findMany({
where: {
id: { in: childrenIds },
kitaId,
familyId: session.user.familyId,
},
select: { id: true },
});
if (validChildren.length !== childrenIds.length) {
return { error: "Ungültige Kinder-Zuordnung erkannt." };
}
// Termine in Date-Objekte wandeln und sortieren
const parsedDates = dates.map(dateKeyToUtcDate);
const hasOutsideTargetMonth = parsedDates.some(
(date) =>
date.getUTCFullYear() !== targetYear ||
date.getUTCMonth() !== targetMonth - 1,
);
if (hasOutsideTargetMonth) {
return { error: "Bitte wähle Termine im Zielmonat aus." };
}
// Round-Robin Zuweisung
const inserts: {
kitaId: string;
childId: string;
userId: string;
date: Date;
}[] = [];
for (let i = 0; i < parsedDates.length; i++) {
const childId = validChildren[i % validChildren.length].id;
inserts.push({
kitaId,
childId,
userId: session.user.id,
date: parsedDates[i],
});
}
// ── 3. Datenbank-Transaktion ───────────────────────────────────────
try {
await prisma.$transaction(async (tx) => {
// a) Alle bestehenden Haushaltseinträge im Zielmonat für diese Kinder löschen
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,
childId: { in: childrenIds },
child: { kitaId, familyId: session.user.familyId! },
date: {
gte: new Date(Date.UTC(targetYear, targetMonth - 1, 1)),
lt: new Date(Date.UTC(targetYear, targetMonth, 1)),
},
},
});
// b) Neue Einträge setzen
if (inserts.length > 0) {
await tx.notdienstAvailability.createMany({
data: inserts,
skipDuplicates: true,
});
}
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) {
console.error("Fehler beim Speichern der Notdienste:", error);
return { error: "Ein Fehler ist aufgetreten beim Speichern der Termine." };
if (!(error instanceof UserFacingError)) console.error("Fehler beim Speichern der Notdienste:", error);
return { error: publicActionError(error, "Ein Fehler ist aufgetreten beim Speichern der Termine.") };
}
}