204 lines
7.7 KiB
TypeScript
204 lines
7.7 KiB
TypeScript
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>
|
||
);
|
||
}
|