Improve family invitations and absence management flows

This commit is contained in:
t.indorf
2026-10-09 22:59:36 +02:00
parent 4702bf5abb
commit 8915fe7df6
29 changed files with 2803 additions and 617 deletions
+203
View File
@@ -0,0 +1,203 @@
import Link from "next/link";
import { requireKitaSession } from "@/lib/auth-utils";
import { prisma } from "@/lib/prisma";
import {
absenceDate,
absenceToday,
canManageKitaAbsences,
} from "@/lib/absence-policy";
import { reasonLabel } from "@/lib/absence-utils";
import { AbsenceManagementActions } from "@/components/absence-management-actions";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
export const metadata = { title: "Meine Abwesenheiten · Kita-Planer" };
export default async function OwnAbsencesPage({
searchParams,
}: {
searchParams: Promise<{ view?: string; page?: string }>;
}) {
const session = await requireKitaSession();
const actor = await prisma.user.findFirstOrThrow({
where: { id: session.user.id, kitaId: session.user.kitaId },
select: { familyId: true, role: true },
});
const query = await searchParams;
const past = query.view === "past";
const parsedPage = Number(query.page ?? 1);
const requestedPage =
Number.isSafeInteger(parsedPage) && parsedPage > 0 ? parsedPage : 1;
const today = absenceToday();
const where = {
kitaId: session.user.kitaId,
child: { kitaId: session.user.kitaId, familyId: actor.familyId ?? "" },
endDate: past ? { lt: absenceDate(today) } : { gte: absenceDate(today) },
};
const count = actor.familyId ? await prisma.absence.count({ where }) : 0;
const pages = Math.max(1, Math.ceil(count / 20));
const page = Math.min(requestedPage, pages);
const absences = actor.familyId
? await prisma.absence.findMany({
where,
select: {
id: true,
startDate: true,
endDate: true,
reason: true,
note: true,
updatedAt: true,
child: { select: { firstName: true, lastName: true } },
},
orderBy: [{ startDate: past ? "desc" : "asc" }, { id: "asc" }],
take: 20,
skip: (page - 1) * 20,
})
: [];
const href = (nextPage: number) =>
`/dashboard/abwesenheiten?view=${past ? "past" : "current"}&page=${nextPage}`;
const dateLabel = (date: Date) =>
new Intl.DateTimeFormat("de-DE", {
dateStyle: "medium",
timeZone: "UTC",
}).format(date);
return (
<div className="mx-auto max-w-[1100px] px-11 py-9 max-[760px]:px-5">
<header className="mb-7">
<p className="text-[11px] font-semibold uppercase tracking-widest text-[var(--accent-deep)]">
MEIN HAUSHALT
</p>
<h1 className="mt-2 text-4xl tracking-tight">
Meine{" "}
<em className="font-normal text-[var(--accent-deep)]">
Abwesenheiten
</em>
</h1>
<p className="mt-3 max-w-2xl text-sm leading-6 text-[var(--ink-soft)]">
Zukünftige Meldungen deiner Kinder kannst du ändern oder zurücknehmen.
Sobald eine Meldung begonnen hat, übernimmt die Kita die gesamte
Meldung.
</p>
<div className="mt-4 flex flex-wrap gap-2">
<Button asChild variant="outline" size="sm">
<Link href="/dashboard">Kind abmelden</Link>
</Button>
{canManageKitaAbsences(actor.role) && (
<Button asChild variant="outline" size="sm">
<Link href="/dashboard/admin/abwesenheiten">
Kita-Tagesübersicht
</Link>
</Button>
)}
</div>
</header>
{!actor.familyId ? (
<Card>
<CardContent className="py-6">
<p className="mb-3">
Deinem Konto ist noch kein Haushalt zugeordnet.
</p>
<Button asChild variant="outline">
<Link href="/dashboard/profil">Profil öffnen</Link>
</Button>
</CardContent>
</Card>
) : (
<>
<nav aria-label="Abwesenheitszeitraum" className="mb-5 flex gap-2">
<Button asChild size="sm" variant={!past ? "default" : "outline"}>
<Link href="/dashboard/abwesenheiten">Aktuell & zukünftig</Link>
</Button>
<Button asChild size="sm" variant={past ? "default" : "outline"}>
<Link href="/dashboard/abwesenheiten?view=past">Vergangen</Link>
</Button>
</nav>
<p className="mb-3 text-sm text-[var(--ink-muted)]">
{count} Meldung{count === 1 ? "" : "en"}
</p>
<div className="grid gap-4">
{absences.length === 0 && (
<Card>
<CardContent className="py-8 text-sm text-[var(--ink-soft)]">
Für diesen Zeitraum sind keine Abwesenheiten gemeldet.
</CardContent>
</Card>
)}
{absences.map((absence) => {
const startDate = absence.startDate.toISOString().slice(0, 10);
return (
<Card key={absence.id}>
<CardContent className="py-5">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h2 className="font-semibold">
{absence.child.firstName} {absence.child.lastName}
</h2>
<p className="mt-1 text-sm text-[var(--ink-soft)]">
{dateLabel(absence.startDate)}
{absence.endDate.getTime() !==
absence.startDate.getTime()
? ` – ${dateLabel(absence.endDate)}`
: ""}{" "}
· {reasonLabel(absence.reason)}
</p>
</div>
<Badge variant="secondary">
{past
? "Vergangen"
: startDate <= today
? "Laufend"
: "Zukünftig"}
</Badge>
</div>
{absence.note && (
<p className="mt-3 whitespace-pre-wrap break-words text-sm text-[var(--ink-soft)]">
{absence.note}
</p>
)}
<AbsenceManagementActions
key={`${absence.id}-${absence.updatedAt.toISOString()}`}
today={today}
team={canManageKitaAbsences(actor.role)}
absence={{
id: absence.id,
reason: absence.reason,
note: absence.note,
childName: `${absence.child.firstName} ${absence.child.lastName}`,
startDate,
endDate: absence.endDate.toISOString().slice(0, 10),
updatedAt: absence.updatedAt.toISOString(),
}}
/>
</CardContent>
</Card>
);
})}
</div>
{pages > 1 && (
<div className="mt-5 flex items-center justify-between gap-3">
{page > 1 ? (
<Button asChild variant="outline">
<Link href={href(page - 1)}>Zurück</Link>
</Button>
) : (
<span />
)}
<span className="text-sm text-[var(--ink-muted)]">
Seite {page} von {pages}
</span>
{page < pages ? (
<Button asChild variant="outline">
<Link href={href(page + 1)}>Weiter</Link>
</Button>
) : (
<span />
)}
</div>
)}
</>
)}
</div>
);
}