continued the kita-planer
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
import Link from "next/link";
|
||||
import { addWeeks, format, getISOWeek, startOfDay, startOfWeek } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { CalendarRange, ClipboardList, Plus, Trash2 } from "lucide-react";
|
||||
import { UserRole } from "@prisma/client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { requireRole } from "@/lib/auth-utils";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { createDutyType, deleteDutyType } from "./actions";
|
||||
import {
|
||||
DutyPlanner,
|
||||
type DutyPlanAssignmentDto,
|
||||
type DutyPlanDutyTypeDto,
|
||||
type DutyPlanFamilyDto,
|
||||
type DutyPlanWeekDto,
|
||||
} from "./duty-planner";
|
||||
|
||||
export const metadata = { title: "Dienstplan · Kita-Planer" };
|
||||
|
||||
const DEFAULT_WEEK_COUNT = 12;
|
||||
const MAX_WEEK_COUNT = 104;
|
||||
|
||||
export default async function DienstePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { start?: string; weeks?: string };
|
||||
}) {
|
||||
const session = await requireRole([UserRole.ADMIN, UserRole.SUPERADMIN]);
|
||||
if (!session.user.kitaId) {
|
||||
throw new Error("Kein Mandant zugeordnet.");
|
||||
}
|
||||
|
||||
const rangeStart = getRangeStart(searchParams.start);
|
||||
const weekCount = getWeekCount(searchParams.weeks);
|
||||
const weeks = buildWeeks(rangeStart, weekCount);
|
||||
const rangeEnd = startOfDay(new Date(weeks[weeks.length - 1].endDate));
|
||||
|
||||
const [dutyTypes, families, assignments] = await Promise.all([
|
||||
prisma.dutyType.findMany({
|
||||
where: { kitaId: session.user.kitaId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
}),
|
||||
prisma.family.findMany({
|
||||
where: { kitaId: session.user.kitaId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
orderBy: { name: "asc" },
|
||||
}),
|
||||
prisma.dutyAssignment.findMany({
|
||||
where: {
|
||||
kitaId: session.user.kitaId,
|
||||
startDate: {
|
||||
gte: rangeStart,
|
||||
lte: rangeEnd,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
dutyTypeId: true,
|
||||
familyId: true,
|
||||
startDate: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const dutyTypeDtos: DutyPlanDutyTypeDto[] = dutyTypes;
|
||||
const familyDtos: DutyPlanFamilyDto[] = families;
|
||||
const assignmentDtos: DutyPlanAssignmentDto[] = assignments.map((assignment) => ({
|
||||
id: assignment.id,
|
||||
dutyTypeId: assignment.dutyTypeId,
|
||||
familyId: assignment.familyId,
|
||||
startDate: toDateKey(assignment.startDate),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
||||
<ClipboardList className="h-4 w-4" />
|
||||
Admin
|
||||
</div>
|
||||
<h1 className="mt-2 text-2xl font-bold tracking-tight">
|
||||
Dienstplan-Manager
|
||||
</h1>
|
||||
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
||||
Plane Dienste pro Kalenderwoche und Haushalt. Jede Änderung wird
|
||||
direkt gespeichert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="w-full lg:max-w-xl">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<CalendarRange className="h-4 w-4" />
|
||||
Zeitraum
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Du kannst beliebig weit voraus planen, jeweils in handlichen
|
||||
Ausschnitten.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="start">Startwoche</Label>
|
||||
<Input
|
||||
id="start"
|
||||
name="start"
|
||||
type="date"
|
||||
defaultValue={toDateInputValue(rangeStart)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="weeks">Wochen</Label>
|
||||
<Input
|
||||
id="weeks"
|
||||
name="weeks"
|
||||
type="number"
|
||||
min={1}
|
||||
max={MAX_WEEK_COUNT}
|
||||
defaultValue={weekCount}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">Anzeigen</Button>
|
||||
<Button asChild type="button" variant="outline">
|
||||
<Link
|
||||
href={`/dashboard/admin/dienste?start=${toDateInputValue(
|
||||
rangeStart,
|
||||
)}&weeks=${Math.min(weekCount + 12, MAX_WEEK_COUNT)}`}
|
||||
>
|
||||
Weitere Wochen laden
|
||||
</Link>
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[340px_1fr]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Diensttypen</CardTitle>
|
||||
<CardDescription>
|
||||
Beispiele sind Wäschedienst, Einkauf oder Gartendienst.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
<form action={createDutyType} className="space-y-3">
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" name="name" placeholder="Wäschedienst" />
|
||||
</div>
|
||||
<div className="grid gap-1.5">
|
||||
<Label htmlFor="description">Beschreibung</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="Kurz beschreiben, was zu tun ist."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" size="sm">
|
||||
<Plus className="h-4 w-4" />
|
||||
Diensttyp anlegen
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="space-y-2">
|
||||
{dutyTypes.length === 0 ? (
|
||||
<p className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
||||
Noch keine Diensttypen angelegt.
|
||||
</p>
|
||||
) : (
|
||||
dutyTypes.map((dutyType) => (
|
||||
<div
|
||||
key={dutyType.id}
|
||||
className="flex items-start justify-between gap-3 rounded-md border p-3"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium">{dutyType.name}</p>
|
||||
{dutyType.description && (
|
||||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{dutyType.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<form action={deleteDutyType}>
|
||||
<input
|
||||
type="hidden"
|
||||
name="dutyTypeId"
|
||||
value={dutyType.id}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={`${dutyType.name} löschen`}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Planung</CardTitle>
|
||||
<CardDescription>
|
||||
{weeks[0].label} bis {weeks[weeks.length - 1].label}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DutyPlanner
|
||||
weeks={weeks}
|
||||
dutyTypes={dutyTypeDtos}
|
||||
families={familyDtos}
|
||||
assignments={assignmentDtos}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getRangeStart(value: string | undefined) {
|
||||
const parsed = value ? new Date(value) : new Date();
|
||||
const safeDate = Number.isNaN(parsed.getTime()) ? new Date() : parsed;
|
||||
return startOfDay(startOfWeek(safeDate, { weekStartsOn: 1 }));
|
||||
}
|
||||
|
||||
function getWeekCount(value: string | undefined) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return DEFAULT_WEEK_COUNT;
|
||||
return Math.min(Math.max(Math.trunc(parsed), 1), MAX_WEEK_COUNT);
|
||||
}
|
||||
|
||||
function buildWeeks(start: Date, count: number): DutyPlanWeekDto[] {
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const weekStart = addWeeks(start, index);
|
||||
const weekEnd = addWeeks(start, index);
|
||||
weekEnd.setDate(weekStart.getDate() + 6);
|
||||
|
||||
return {
|
||||
key: toDateKey(weekStart),
|
||||
label: `KW ${getISOWeek(weekStart)}: ${format(weekStart, "dd.MM.", {
|
||||
locale: de,
|
||||
})} - ${format(weekEnd, "dd.MM.", { locale: de })}`,
|
||||
startDate: toDateKey(weekStart),
|
||||
endDate: toDateKey(weekEnd),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function toDateKey(date: Date) {
|
||||
return format(date, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
function toDateInputValue(date: Date) {
|
||||
return format(date, "yyyy-MM-dd");
|
||||
}
|
||||
Reference in New Issue
Block a user