Files

366 lines
14 KiB
TypeScript

"use client";
import { useState, useMemo } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { addDays, format, parseISO } from "date-fns";
import { de } from "date-fns/locale";
import {
ArrowLeft,
ArrowRight,
CalendarCheck2,
CalendarDays,
ChevronDown,
ChevronRight,
Stethoscope,
} from "lucide-react";
import { AbsenceReason } from "@prisma/client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { AbsenceTable, type AbsenceTableItem } from "../absence-table";
import { reasonColor, reasonLabel } from "@/lib/absence-utils";
interface AbsencesClientProps {
initialAbsences: AbsenceTableItem[];
childCount: number;
upcomingAbsenceCount: number;
closure: { title: string; type: string } | null;
dateKey: string;
selectedDateStr: string;
isToday: boolean;
isTooFarFuture: boolean;
isClosedDay: boolean;
todayStr: string;
error: string | null;
}
export function AbsencesClient({
initialAbsences,
childCount,
upcomingAbsenceCount,
closure,
dateKey,
selectedDateStr,
isToday,
isTooFarFuture,
isClosedDay,
todayStr,
error,
}: AbsencesClientProps) {
const router = useRouter();
const selectedDate = parseISO(selectedDateStr);
const today = parseISO(todayStr);
// Filters State
const [selectedReason, setSelectedReason] = useState<"all" | AbsenceReason>("all");
// Dropdown UI state
const [isReasonOpen, setIsReasonOpen] = useState(false);
// Date input state
const [inputDate, setInputDate] = useState(dateKey);
// Filter computation
const filteredAbsences = useMemo(() => {
return initialAbsences.filter((item) => {
if (selectedReason !== "all" && item.reason !== selectedReason) {
return false;
}
return true;
});
}, [initialAbsences, selectedReason]);
// Recalculate reasons count based on filtered absences
const countsByReason = useMemo(() => {
return filteredAbsences.reduce<Partial<Record<AbsenceReason, number>>>((acc, item) => {
acc[item.reason] = (acc[item.reason] ?? 0) + 1;
return acc;
}, {});
}, [filteredAbsences]);
const handleDateNavigate = (targetDate: Date) => {
const formatted = format(targetDate, "yyyy-MM-dd");
setInputDate(formatted);
router.push(`/dashboard/admin/abwesenheiten?date=${formatted}`);
};
const handleDateFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (inputDate) {
router.push(`/dashboard/admin/abwesenheiten?date=${inputDate}`);
}
};
return (
<div>
<header className="mb-7 flex items-end justify-between gap-8 max-[1100px]:flex-col max-[1100px]:items-start">
<div>
<p className="text-[11px] font-semibold uppercase text-[var(--accent-deep)] [letter-spacing:0.18em]">
TAGESÜBERSICHT
</p>
<h1 className="mt-2 text-[36px] font-normal leading-tight tracking-[-0.025em] text-[var(--ink)]">
{isToday ? "Heute" : "An diesem Tag"}{filteredAbsences.length === 1 ? " fehlt " : " fehlen "}
<em className="font-normal italic text-[var(--accent-deep)]">
{filteredAbsences.length} {filteredAbsences.length === 1 ? "Kind" : "Kinder"}
</em>
</h1>
<p className="mt-2 text-[15px] leading-6 text-[var(--ink-soft)]">
{format(selectedDate, "EEEE, d. MMMM yyyy", { locale: de })}
</p>
</div>
<div className="flex flex-wrap items-center justify-end gap-3">
<span className="inline-flex h-9 items-center gap-2 rounded-full border border-[var(--hairline)] bg-[var(--surface)] px-3 text-[13px] font-semibold text-[var(--ink-soft)]">
<Stethoscope className="h-4 w-4 text-[var(--accent-deep)] [stroke-width:1.7]" />
{Math.max(0, childCount - initialAbsences.length)} anwesend
</span>
<div className="inline-flex overflow-hidden rounded-full border border-[var(--hairline)] bg-[var(--surface)]">
<button
onClick={() => handleDateNavigate(addDays(selectedDate, -1))}
className="inline-flex h-10 items-center gap-2 border-r border-[var(--hairline)] px-4 text-[13px] font-semibold text-[var(--ink-soft)] transition hover:bg-[var(--surface-2)] hover:text-[var(--ink)]"
>
<ArrowLeft className="h-3.5 w-3.5" />
Zurück
</button>
<button
onClick={() => handleDateNavigate(today)}
className={`inline-flex h-10 items-center gap-2 border-r border-[var(--hairline)] px-4 text-[13px] font-semibold text-[var(--ink-soft)] transition hover:bg-[var(--surface-2)] hover:text-[var(--ink)] ${
isToday ? "bg-[var(--accent-soft)] text-[var(--accent-deep)]" : ""
}`}
>
Heute
</button>
<button
onClick={() => handleDateNavigate(addDays(selectedDate, 1))}
className="inline-flex h-10 items-center gap-2 px-4 text-[13px] font-semibold text-[var(--ink-soft)] transition hover:bg-[var(--surface-2)] hover:text-[var(--ink)]"
>
Vor
<ArrowRight className="h-3.5 w-3.5" />
</button>
</div>
<form onSubmit={handleDateFormSubmit} className="flex items-center gap-2">
<Input
type="date"
name="date"
value={inputDate}
onChange={(e) => setInputDate(e.target.value)}
className="w-[140px]"
/>
<Button type="submit">Anzeigen</Button>
</form>
</div>
</header>
<div className="mt-5 flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap gap-2">
{/* Reason Dropdown Filter */}
<div className="relative">
<button
type="button"
onClick={() => setIsReasonOpen(!isReasonOpen)}
className={`inline-flex h-9 items-center gap-2 rounded-full border px-4 text-[13px] font-semibold transition focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--focus-ring)] ${
selectedReason !== "all"
? "border-[var(--accent-mid)] bg-[var(--accent-soft)] text-[var(--accent-deep)]"
: "border-[var(--hairline)] bg-transparent text-[var(--ink-soft)] hover:bg-[var(--surface-2)]"
}`}
>
{selectedReason === "all" ? "Alle Gründe" : reasonLabel(selectedReason)}
<ChevronDown className="h-3.5 w-3.5" />
</button>
{isReasonOpen && (
<>
<div className="fixed inset-0 z-10" onClick={() => setIsReasonOpen(false)} />
<div className="absolute left-0 mt-1.5 w-44 rounded-lg border border-[var(--hairline)] bg-[var(--surface)] py-1.5 shadow-lg z-20">
<button
type="button"
onClick={() => {
setSelectedReason("all");
setIsReasonOpen(false);
}}
className={`w-full text-left px-4 py-2 text-[13px] transition-colors ${
selectedReason === "all"
? "bg-[var(--accent-soft)] text-[var(--accent-deep)] font-semibold"
: "text-[var(--ink-soft)] hover:bg-[var(--surface-2)]"
}`}
>
Alle Gründe
</button>
<button
type="button"
onClick={() => {
setSelectedReason(AbsenceReason.ILLNESS);
setIsReasonOpen(false);
}}
className={`w-full text-left px-4 py-2 text-[13px] transition-colors ${
selectedReason === AbsenceReason.ILLNESS
? "bg-[var(--accent-soft)] text-[var(--accent-deep)] font-semibold"
: "text-[var(--ink-soft)] hover:bg-[var(--surface-2)]"
}`}
>
Krankheit
</button>
<button
type="button"
onClick={() => {
setSelectedReason(AbsenceReason.VACATION);
setIsReasonOpen(false);
}}
className={`w-full text-left px-4 py-2 text-[13px] transition-colors ${
selectedReason === AbsenceReason.VACATION
? "bg-[var(--accent-soft)] text-[var(--accent-deep)] font-semibold"
: "text-[var(--ink-soft)] hover:bg-[var(--surface-2)]"
}`}
>
Urlaub
</button>
<button
type="button"
onClick={() => {
setSelectedReason(AbsenceReason.OTHER);
setIsReasonOpen(false);
}}
className={`w-full text-left px-4 py-2 text-[13px] transition-colors ${
selectedReason === AbsenceReason.OTHER
? "bg-[var(--accent-soft)] text-[var(--accent-deep)] font-semibold"
: "text-[var(--ink-soft)] hover:bg-[var(--surface-2)]"
}`}
>
Sonstiges
</button>
</div>
</>
)}
</div>
</div>
{filteredAbsences.length > 0 ? (
<div className="flex flex-wrap items-center gap-4 text-xs text-[var(--ink-soft)]">
<span className="inline-flex items-center gap-1.5">
<span
className="h-2 w-2 rounded-full"
style={{ backgroundColor: reasonColor(AbsenceReason.ILLNESS) }}
/>
{reasonLabel(AbsenceReason.ILLNESS)} {countsByReason[AbsenceReason.ILLNESS] ?? 0}
</span>
<span className="inline-flex items-center gap-1.5">
<span
className="h-2 w-2 rounded-full"
style={{ backgroundColor: reasonColor(AbsenceReason.OTHER) }}
/>
{reasonLabel(AbsenceReason.OTHER)} {countsByReason[AbsenceReason.OTHER] ?? 0}
</span>
<span className="inline-flex items-center gap-1.5">
<span
className="h-2 w-2 rounded-full"
style={{ backgroundColor: reasonColor(AbsenceReason.VACATION) }}
/>
{reasonLabel(AbsenceReason.VACATION)} {countsByReason[AbsenceReason.VACATION] ?? 0}
</span>
<span>
letzte Aktualisierung {format(new Date(), "HH:mm", { locale: de })}
</span>
</div>
) : null}
</div>
<div className="mt-4">
{error ? (
<Card className="border-[var(--danger)] bg-[var(--danger-soft)]">
<CardContent className="pt-[22px] text-sm text-[var(--danger)]">
{error}
</CardContent>
</Card>
) : filteredAbsences.length > 0 ? (
<Card>
<CardHeader className="pb-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<CardTitle>
{filteredAbsences.length} von {childCount} Kindern abwesend ·{" "}
{upcomingAbsenceCount} anstehend
</CardTitle>
<Badge variant="secondary">{dateKey}</Badge>
</div>
</CardHeader>
<CardContent>
<AbsenceTable absences={filteredAbsences} today={todayStr} />
</CardContent>
</Card>
) : isTooFarFuture ? (
<EmptyState
tone="accent"
title="Noch nicht im Vorschauzeitraum"
description="Abwesenheiten werden erst 30 Tage im Voraus angezeigt."
/>
) : isClosedDay ? (
<EmptyState
tone="accent"
title={`Kita geschlossen am ${format(selectedDate, "d. MMMM", {
locale: de,
})}`}
description={
closure
? closure.title
: "Wochenende · für diesen Tag werden keine Anwesenheiten erwartet."
}
/>
) : (
<EmptyState
tone="good"
title="Keine Abwesenheiten"
description="Für diesen Tag sind aktuell keine Kinder abgemeldet."
/>
)}
</div>
</div>
);
}
function EmptyState({
tone,
title,
description,
}: {
tone: "good" | "accent";
title: string;
description: string;
}) {
const isGood = tone === "good";
return (
<Card>
<CardContent className="flex max-h-[280px] min-h-[260px] flex-col items-center justify-center px-8 py-12 text-center">
<div
className={[
"flex h-12 w-12 items-center justify-center rounded-2xl",
isGood
? "bg-[var(--good-soft)] text-[var(--good)]"
: "bg-[var(--accent-soft)] text-[var(--accent-deep)]",
].join(" ")}
>
{isGood ? (
<CalendarCheck2 className="h-6 w-6 [stroke-width:1.7]" />
) : (
<CalendarDays className="h-6 w-6 [stroke-width:1.7]" />
)}
</div>
<h2 className="mt-5 text-lg font-medium tracking-[-0.02em] text-[var(--ink)]">
{title}
</h2>
<p className="mt-2 max-w-md text-sm text-[var(--ink-soft)]">
{description}
</p>
<Link
href="/dashboard/admin/abwesenheiten"
className="mt-5 inline-flex items-center gap-1 text-sm font-semibold text-[var(--accent-deep)] transition hover:gap-2 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--focus-ring)]"
>
Wochenübersicht öffnen
<ChevronRight className="h-3.5 w-3.5" />
</Link>
</CardContent>
</Card>
);
}