Files
kita-planer/src/app/dashboard/abwesenheiten/page.tsx
T

204 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}