feat: show planning availability and manage Kita access roles

This commit is contained in:
t.indorf
2026-10-12 16:28:56 +02:00
parent 99d3a63eed
commit 061024d901
29 changed files with 2347 additions and 404 deletions
+57 -30
View File
@@ -4,7 +4,11 @@ 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 {
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";
@@ -108,8 +112,6 @@ export default async function PlanungsZentralePage({
]);
// Wir laden auch alle aktiven Kinder (inkl. Eltern-Namen) für das manuelle Dropdown.
// Idealerweise gruppiert man das nach "Verfügbar an diesem Tag" und "Alle",
// aber für MVP reichen alle aktiven Kinder.
const allChildrenRaw = await prisma.child.findMany({
where: { kitaId, active: true },
select: {
@@ -146,8 +148,11 @@ export default async function PlanungsZentralePage({
});
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 availabilities = rawAvailabilities.filter((a) =>
validDateKeys.has(formatUtcDateKey(a.date)),
);
const closureCount =
getWorkingDaysOfMonth(targetYear, targetMonth).length - workingDays.length;
const availabilityDatesByFamily = new Map<string, Set<string>>();
availabilities.forEach((availability) => {
@@ -186,37 +191,52 @@ export default async function PlanungsZentralePage({
// 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]));
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 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 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 mailStatus = await getNotdienstEmailStatus(
kitaId,
targetYear,
targetMonth,
);
const target = getTargetMonthData(now);
const remindersOpen = !target.isLocked && target.targetYear === targetYear && target.targetMonth === targetMonth && plan?.status !== "PUBLISHED";
const remindersOpen =
!target.isLocked &&
target.targetYear === targetYear &&
target.targetMonth === targetMonth &&
plan?.status !== "PUBLISHED";
return (
<div className="mx-auto max-w-[1280px] px-11 py-9 pb-16 max-[760px]:px-5">
@@ -261,7 +281,14 @@ export default async function PlanungsZentralePage({
</div>
</header>
<EmailStatusCard status={mailStatus} year={targetYear} month={targetMonth} remindersOpen={remindersOpen} daysBefore={kita.notdienstReminderDaysBefore} jobConfigured={(process.env.NOTDIENST_JOB_SECRET?.length ?? 0) >= 32} />
<EmailStatusCard
status={mailStatus}
year={targetYear}
month={targetMonth}
remindersOpen={remindersOpen}
daysBefore={kita.notdienstReminderDaysBefore}
jobConfigured={(process.env.NOTDIENST_JOB_SECRET?.length ?? 0) >= 32}
/>
<PlanView
planId={plan?.id}