continued the kita-planer
This commit is contained in:
@@ -47,6 +47,9 @@ export async function saveNotdienstAvailabilities(payloadRaw: {
|
||||
if (!kitaId) {
|
||||
return { error: "Kein Mandant zugeordnet." };
|
||||
}
|
||||
if (!session.user.familyId) {
|
||||
return { error: "Dein Account ist noch keinem Haushalt zugeordnet." };
|
||||
}
|
||||
|
||||
// ── 2. Validierung (Soll-Tage) ─────────────────────────────────────
|
||||
const kita = await prisma.kita.findUniqueOrThrow({
|
||||
@@ -61,12 +64,12 @@ export async function saveNotdienstAvailabilities(payloadRaw: {
|
||||
};
|
||||
}
|
||||
|
||||
// Sicherstellen, dass alle Kinder zum angemeldeten User gehören und zur kitaId passen
|
||||
// Sicherstellen, dass alle Kinder zum Haushalt des angemeldeten Users gehören.
|
||||
const validChildren = await prisma.child.findMany({
|
||||
where: {
|
||||
id: { in: childrenIds },
|
||||
kitaId,
|
||||
parentLinks: { some: { userId: session.user.id } },
|
||||
familyId: session.user.familyId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
@@ -101,11 +104,10 @@ export async function saveNotdienstAvailabilities(payloadRaw: {
|
||||
// ── 3. Datenbank-Transaktion ───────────────────────────────────────
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// a) Alle *eigenen* bestehenden Einträge im Zielmonat für diese Kinder löschen
|
||||
// a) Alle bestehenden Haushaltseinträge im Zielmonat für diese Kinder löschen
|
||||
await tx.notdienstAvailability.deleteMany({
|
||||
where: {
|
||||
kitaId,
|
||||
userId: session.user.id,
|
||||
childId: { in: childrenIds },
|
||||
date: {
|
||||
gte: new Date(targetYear, targetMonth - 1, 1),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ArrowRight, CalendarHeart } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NotdienstForm } from "./notdienst-form";
|
||||
|
||||
type NotdienstEntryProps = {
|
||||
hasAnyAvailability: boolean;
|
||||
targetYear: number;
|
||||
targetMonth: number;
|
||||
isLocked: boolean;
|
||||
requiredDaysTotal: number;
|
||||
initialSelectedDates: string[];
|
||||
childrenIds: string[];
|
||||
};
|
||||
|
||||
export function NotdienstEntry({
|
||||
hasAnyAvailability,
|
||||
targetYear,
|
||||
targetMonth,
|
||||
isLocked,
|
||||
requiredDaysTotal,
|
||||
initialSelectedDates,
|
||||
childrenIds,
|
||||
}: NotdienstEntryProps) {
|
||||
const [showForm, setShowForm] = useState(hasAnyAvailability);
|
||||
|
||||
if (!showForm) {
|
||||
return (
|
||||
<div className="flex min-h-[420px] items-center justify-center rounded-lg border border-dashed bg-gradient-to-b from-emerald-50/70 to-background p-8 text-center">
|
||||
<div className="mx-auto max-w-lg">
|
||||
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-emerald-100 text-emerald-700 shadow-sm">
|
||||
<CalendarHeart className="h-10 w-10" />
|
||||
</div>
|
||||
<h2 className="mt-6 text-2xl font-semibold tracking-tight">
|
||||
Willkommen beim Notdienst!
|
||||
</h2>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
Es sieht so aus, als hättest du noch keine Termine eingetragen.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-6"
|
||||
onClick={() => setShowForm(true)}
|
||||
>
|
||||
Jetzt erste Pflicht-Termine eintragen
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NotdienstForm
|
||||
targetYear={targetYear}
|
||||
targetMonth={targetMonth}
|
||||
isLocked={isLocked}
|
||||
requiredDaysTotal={requiredDaysTotal}
|
||||
initialSelectedDates={initialSelectedDates}
|
||||
childrenIds={childrenIds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import { UserRole } from "@prisma/client";
|
||||
import { Info, Calendar } from "lucide-react";
|
||||
import { Info, Calendar, ArrowRight, ShieldAlert } from "lucide-react";
|
||||
|
||||
import { requireRole } from "@/lib/auth-utils";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getTargetMonthData } from "@/lib/date-utils";
|
||||
import { NotdienstForm } from "./notdienst-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NotdienstEntry } from "./notdienst-entry";
|
||||
|
||||
export const metadata = { title: "Notdienst · Kita-Planer" };
|
||||
|
||||
@@ -29,34 +31,51 @@ export default async function NotdienstPage() {
|
||||
select: { notdienstMinPerChildPerMonth: true },
|
||||
});
|
||||
|
||||
// Nur eigene, aktive Kinder laden
|
||||
const children = await prisma.child.findMany({
|
||||
where: {
|
||||
kitaId,
|
||||
active: true,
|
||||
parentLinks: { some: { userId: session.user.id } },
|
||||
},
|
||||
select: { id: true, firstName: true },
|
||||
});
|
||||
// Nur aktive Kinder des eigenen Haushalts laden.
|
||||
const children = session.user.familyId
|
||||
? await prisma.child.findMany({
|
||||
where: {
|
||||
kitaId,
|
||||
familyId: session.user.familyId,
|
||||
active: true,
|
||||
},
|
||||
select: { id: true, firstName: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const targetData = getTargetMonthData();
|
||||
const { targetYear, targetMonth, monthName, isLocked } = targetData;
|
||||
|
||||
const requiredDaysTotal = children.length * kita.notdienstMinPerChildPerMonth;
|
||||
const childIds = children.map((child) => child.id);
|
||||
|
||||
// Bisherige Einträge für den Zielmonat laden
|
||||
// Wir filtern bewusst nach den eigenen Kindern und dem User
|
||||
const availabilities = await prisma.notdienstAvailability.findMany({
|
||||
where: {
|
||||
kitaId,
|
||||
userId: session.user.id,
|
||||
date: {
|
||||
gte: new Date(targetYear, targetMonth - 1, 1),
|
||||
lt: new Date(targetYear, targetMonth, 1),
|
||||
},
|
||||
},
|
||||
select: { date: true },
|
||||
});
|
||||
// Bisherige Einträge für den Zielmonat laden, haushaltsweit über die Kinder.
|
||||
const [availabilities, availabilityCountForCurrentOrNextMonth] =
|
||||
children.length > 0
|
||||
? await Promise.all([
|
||||
prisma.notdienstAvailability.findMany({
|
||||
where: {
|
||||
kitaId,
|
||||
childId: { in: childIds },
|
||||
date: {
|
||||
gte: new Date(targetYear, targetMonth - 1, 1),
|
||||
lt: new Date(targetYear, targetMonth, 1),
|
||||
},
|
||||
},
|
||||
select: { date: true },
|
||||
}),
|
||||
prisma.notdienstAvailability.count({
|
||||
where: {
|
||||
kitaId,
|
||||
childId: { in: childIds },
|
||||
date: {
|
||||
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
|
||||
lt: new Date(new Date().getFullYear(), new Date().getMonth() + 2, 1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
: [[], 0];
|
||||
|
||||
const selectedDates = availabilities.map((a) => a.date.toISOString());
|
||||
|
||||
@@ -86,19 +105,43 @@ export default async function NotdienstPage() {
|
||||
</div>
|
||||
|
||||
{children.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed p-8 text-center text-muted-foreground">
|
||||
Deinem Account sind noch keine Kinder zugeordnet.
|
||||
</div>
|
||||
<NotdienstLockout />
|
||||
) : (
|
||||
<NotdienstForm
|
||||
<NotdienstEntry
|
||||
hasAnyAvailability={availabilityCountForCurrentOrNextMonth > 0}
|
||||
targetYear={targetYear}
|
||||
targetMonth={targetMonth}
|
||||
isLocked={isLocked}
|
||||
requiredDaysTotal={requiredDaysTotal}
|
||||
initialSelectedDates={selectedDates}
|
||||
childrenIds={children.map((c) => c.id)}
|
||||
childrenIds={childIds}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotdienstLockout() {
|
||||
return (
|
||||
<div className="flex min-h-[420px] items-center justify-center rounded-lg border border-dashed bg-muted/20 p-8 text-center">
|
||||
<div className="mx-auto max-w-lg">
|
||||
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-amber-100 text-amber-700 shadow-sm">
|
||||
<ShieldAlert className="h-10 w-10" />
|
||||
</div>
|
||||
<h2 className="mt-6 text-2xl font-semibold tracking-tight">
|
||||
Erst Kinder hinterlegen
|
||||
</h2>
|
||||
<p className="mt-3 text-sm leading-6 text-muted-foreground">
|
||||
Bitte lege zuerst deine Kinder im Profil an, bevor du Notdienste
|
||||
übernimmst.
|
||||
</p>
|
||||
<Button asChild className="mt-6">
|
||||
<Link href="/dashboard/profil">
|
||||
Zum Profil
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function generatePlanAction() {
|
||||
lt: new Date(targetYear, targetMonth, 1),
|
||||
},
|
||||
},
|
||||
include: { child: true, user: true },
|
||||
include: { child: { select: { familyId: true } } },
|
||||
});
|
||||
|
||||
// 2. Werktage holen
|
||||
@@ -63,8 +63,8 @@ export async function generatePlanAction() {
|
||||
// Finde die Familie, die am wenigsten oft dran war
|
||||
// Zufall einbauen bei Gleichstand
|
||||
const sorted = availForDay.sort((a, b) => {
|
||||
const countA = familyUsageCount[a.userId] || 0;
|
||||
const countB = familyUsageCount[b.userId] || 0;
|
||||
const countA = familyUsageCount[a.child.familyId] || 0;
|
||||
const countB = familyUsageCount[b.child.familyId] || 0;
|
||||
if (countA === countB) {
|
||||
return Math.random() - 0.5; // Zufallsmischung
|
||||
}
|
||||
@@ -79,8 +79,8 @@ export async function generatePlanAction() {
|
||||
});
|
||||
|
||||
// Usage-Counter erhöhen
|
||||
familyUsageCount[selected.userId] =
|
||||
(familyUsageCount[selected.userId] || 0) + 1;
|
||||
familyUsageCount[selected.child.familyId] =
|
||||
(familyUsageCount[selected.child.familyId] || 0) + 1;
|
||||
}
|
||||
|
||||
// 4. In der DB speichern
|
||||
@@ -161,6 +161,19 @@ export async function updateAssignmentAction(
|
||||
|
||||
// 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,
|
||||
@@ -174,7 +187,9 @@ export async function updateAssignmentAction(
|
||||
|
||||
revalidatePath("/dashboard/notdienst/plan");
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
return { error: error.message || "Update fehlgeschlagen." };
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error instanceof Error ? error.message : "Update fehlgeschlagen.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,15 @@ export default async function PlanungsZentralePage() {
|
||||
where: {
|
||||
kitaId_year_month: { kitaId, year: targetYear, month: targetMonth },
|
||||
},
|
||||
include: {
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
assignments: {
|
||||
include: { child: { include: { parentLinks: { include: { user: true } } } } },
|
||||
select: {
|
||||
id: true,
|
||||
childId: true,
|
||||
date: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -34,16 +40,30 @@ export default async function PlanungsZentralePage() {
|
||||
// aber für MVP reichen alle aktiven Kinder.
|
||||
const allChildrenRaw = await prisma.child.findMany({
|
||||
where: { kitaId, active: true },
|
||||
include: {
|
||||
parentLinks: { include: { user: true } },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
family: {
|
||||
select: {
|
||||
name: true,
|
||||
users: {
|
||||
select: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { lastName: "asc" },
|
||||
});
|
||||
|
||||
const allChildren = allChildrenRaw.map((c) => {
|
||||
// Einfachheit halber nehmen wir den ersten verknüpften Elternteil zur Anzeige
|
||||
const parent = c.parentLinks[0]?.user;
|
||||
const parentName = parent ? `${parent.firstName} ${parent.lastName}` : "Unbekannt";
|
||||
const parentName =
|
||||
c.family.users
|
||||
.map((parent) => `${parent.firstName} ${parent.lastName}`)
|
||||
.join(", ") || c.family.name;
|
||||
return {
|
||||
id: c.id,
|
||||
name: `${c.firstName} ${c.lastName}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useTransition } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { NotdienstPlanStatus } from "@prisma/client";
|
||||
|
||||
Reference in New Issue
Block a user