Apply linen design system across app
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { UserRole } from "@prisma/client";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { TerminType, UserRole } from "@prisma/client";
|
||||
import { ChevronLeft, ChevronRight, SlidersHorizontal } from "lucide-react";
|
||||
|
||||
import { requireRole } from "@/lib/auth-utils";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { getTargetMonthData, getWorkingDaysOfMonth } from "@/lib/date-utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PlanView } from "./plan-view";
|
||||
|
||||
export const metadata = { title: "Plan-Generierung · Kita-Planer" };
|
||||
@@ -14,26 +15,71 @@ export default async function PlanungsZentralePage() {
|
||||
|
||||
const { targetYear, targetMonth, monthName } = getTargetMonthData();
|
||||
|
||||
// Aktuellen Plan suchen
|
||||
const plan = await prisma.notdienstPlan.findUnique({
|
||||
where: {
|
||||
kitaId_year_month: { kitaId, year: targetYear, month: targetMonth },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
assignments: {
|
||||
select: {
|
||||
id: true,
|
||||
childId: true,
|
||||
date: true,
|
||||
const workingDays = getWorkingDaysOfMonth(targetYear, targetMonth);
|
||||
const monthStart = new Date(targetYear, targetMonth - 1, 1);
|
||||
const monthEnd = new Date(targetYear, targetMonth, 1);
|
||||
|
||||
const [plan, families, availabilities, closureCount] = await Promise.all([
|
||||
prisma.notdienstPlan.findUnique({
|
||||
where: {
|
||||
kitaId_year_month: { kitaId, year: targetYear, month: targetMonth },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
assignments: {
|
||||
select: {
|
||||
id: true,
|
||||
childId: true,
|
||||
date: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Alle Werktage
|
||||
const workingDays = getWorkingDaysOfMonth(targetYear, targetMonth);
|
||||
}),
|
||||
prisma.family.findMany({
|
||||
where: {
|
||||
kitaId,
|
||||
children: { some: { active: true } },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
children: {
|
||||
where: { active: true },
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
}),
|
||||
prisma.notdienstAvailability.findMany({
|
||||
where: {
|
||||
kitaId,
|
||||
date: {
|
||||
gte: monthStart,
|
||||
lt: monthEnd,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
date: true,
|
||||
child: {
|
||||
select: {
|
||||
familyId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { date: "asc" },
|
||||
}),
|
||||
prisma.termin.count({
|
||||
where: {
|
||||
kitaId,
|
||||
type: TerminType.SCHLIESSTAG,
|
||||
startDate: {
|
||||
gte: monthStart,
|
||||
lt: monthEnd,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// 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",
|
||||
@@ -71,6 +117,40 @@ export default async function PlanungsZentralePage() {
|
||||
};
|
||||
});
|
||||
|
||||
const availabilityDatesByFamily = new Map<string, Set<string>>();
|
||||
availabilities.forEach((availability) => {
|
||||
const familyId = availability.child.familyId;
|
||||
const dates = availabilityDatesByFamily.get(familyId) ?? new Set<string>();
|
||||
dates.add(availability.date.toISOString());
|
||||
availabilityDatesByFamily.set(familyId, dates);
|
||||
});
|
||||
|
||||
const familyStatuses = families.map((family) => {
|
||||
const dates = Array.from(availabilityDatesByFamily.get(family.id) ?? []);
|
||||
return {
|
||||
id: family.id,
|
||||
name: family.name,
|
||||
selectedDays: dates.map((date) => ({
|
||||
iso: date,
|
||||
label: new Intl.DateTimeFormat("de-DE", {
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
}).format(new Date(date)),
|
||||
})),
|
||||
childCount: family.children.length,
|
||||
};
|
||||
});
|
||||
|
||||
const respondedFamilies = familyStatuses.filter(
|
||||
(family) => family.selectedDays.length > 0,
|
||||
).length;
|
||||
const openFamilies = Math.max(familyStatuses.length - respondedFamilies, 0);
|
||||
const totalAvailabilitySlots = availabilities.length;
|
||||
const requiredDays = workingDays.length;
|
||||
const coverageFactor =
|
||||
requiredDays > 0 ? totalAvailabilitySlots / requiredDays : 0;
|
||||
|
||||
// Wir mappen die Assignments auf die Werktage
|
||||
const daysWithAssignments = workingDays.map((day) => {
|
||||
const assignment = plan?.assignments.find(
|
||||
@@ -84,29 +164,56 @@ export default async function PlanungsZentralePage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl px-4 py-8 sm:px-8">
|
||||
<div className="mb-6 flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div className="mx-auto max-w-[1280px] px-11 py-9 pb-16 max-[760px]:px-5">
|
||||
<header className="mb-7 flex items-end justify-between gap-8 max-[900px]:items-start max-[900px]:flex-col">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
||||
<Settings2 className="h-6 w-6 text-primary" />
|
||||
Notdienst-Planung
|
||||
<p className="text-[11px] font-semibold uppercase text-[var(--accent-deep)] [letter-spacing:0.18em]">
|
||||
NOTDIENST · PLANUNG
|
||||
</p>
|
||||
<h1 className="mt-2 text-[36px] font-normal leading-tight tracking-[-0.025em] text-[var(--ink)]">
|
||||
Notdienst{" "}
|
||||
<em className="font-normal italic text-[var(--accent-deep)]">
|
||||
zuweisen
|
||||
</em>
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<p className="mt-2 max-w-2xl text-[15px] leading-6 text-[var(--ink-soft)]">
|
||||
Koordiniere und generiere den Notdienst-Plan.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full bg-primary/10 px-3 py-1 text-sm font-medium text-primary">
|
||||
Zielmonat: {monthName}
|
||||
</span>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<div className="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)]">
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9 rounded-none border-r border-[var(--hairline-soft)]">
|
||||
<ChevronLeft className="h-4 w-4 [stroke-width:1.7]" />
|
||||
</Button>
|
||||
<div className="flex h-9 min-w-32 items-center justify-center px-4 text-[13px] font-semibold text-[var(--ink)]">
|
||||
{monthName} {targetYear}
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9 rounded-none border-l border-[var(--hairline-soft)]">
|
||||
<ChevronRight className="h-4 w-4 [stroke-width:1.7]" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
<SlidersHorizontal className="h-4 w-4 [stroke-width:1.7]" />
|
||||
Einstellungen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<PlanView
|
||||
planId={plan?.id}
|
||||
status={plan?.status}
|
||||
days={daysWithAssignments}
|
||||
allChildren={allChildren}
|
||||
familyStatuses={familyStatuses}
|
||||
requiredDays={requiredDays}
|
||||
respondedFamilies={respondedFamilies}
|
||||
openFamilies={openFamilies}
|
||||
totalFamilies={familyStatuses.length}
|
||||
totalAvailabilitySlots={totalAvailabilitySlots}
|
||||
coverageFactor={coverageFactor}
|
||||
closureCount={closureCount}
|
||||
monthName={monthName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user