196 lines
6.1 KiB
TypeScript
196 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { AbsenceReason } from "@prisma/client";
|
|
import { CalendarDays, Circle, MessageSquareText, UserRound } from "lucide-react";
|
|
import { reasonColor, reasonLabel } from "@/lib/absence-utils";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
|
|
export type AbsenceTableItem = {
|
|
id: string;
|
|
childName: string;
|
|
familyName: string;
|
|
reason: AbsenceReason;
|
|
note: string | null;
|
|
startDate: string;
|
|
endDate: string;
|
|
};
|
|
|
|
export function AbsenceTable({ absences }: { absences: AbsenceTableItem[] }) {
|
|
const [activeAbsence, setActiveAbsence] = useState<AbsenceTableItem | null>(
|
|
null,
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableHead>Kind</TableHead>
|
|
<TableHead>Zeitraum</TableHead>
|
|
<TableHead>Grund</TableHead>
|
|
<TableHead>Bemerkung</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{absences.map((absence) => (
|
|
<TableRow
|
|
key={absence.id}
|
|
className="cursor-pointer border-[var(--hairline-soft)] hover:bg-[var(--surface-2)]"
|
|
onClick={() => setActiveAbsence(absence)}
|
|
>
|
|
<TableCell>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--accent-soft)] text-xs font-semibold text-[var(--accent-deep)]">
|
|
{getInitials(absence.childName)}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="truncate font-semibold text-[var(--ink)]">
|
|
{absence.childName}
|
|
</p>
|
|
<p className="truncate text-xs text-[var(--ink-soft)]">
|
|
{absence.familyName}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-[var(--ink-soft)]">
|
|
{formatDateRange(absence.startDate, absence.endDate)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="inline-flex items-center gap-2 text-[var(--ink)]">
|
|
<span
|
|
className="h-2 w-2 rounded-full"
|
|
style={{ backgroundColor: reasonColor(absence.reason) }}
|
|
/>
|
|
{reasonLabel(absence.reason)}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="max-w-[280px] truncate text-[var(--ink-soft)]">
|
|
{absence.note || "Keine Bemerkung hinterlegt."}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="success">Bestätigt</Badge>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
|
|
<Dialog
|
|
open={!!activeAbsence}
|
|
onOpenChange={(open) => !open && setActiveAbsence(null)}
|
|
>
|
|
<DialogContent className="left-auto right-0 top-0 h-full max-w-md translate-x-0 translate-y-0 rounded-none border-y-0 border-r-0 border-[var(--hairline)] bg-[var(--surface)] data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right">
|
|
{activeAbsence ? (
|
|
<>
|
|
<DialogHeader>
|
|
<DialogTitle className="text-2xl font-medium tracking-[-0.02em] text-[var(--ink)]">
|
|
{activeAbsence.childName}
|
|
</DialogTitle>
|
|
<DialogDescription>{activeAbsence.familyName}</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="mt-4 grid gap-4">
|
|
<DetailLine
|
|
icon={CalendarDays}
|
|
label="Zeitraum"
|
|
value={formatDateRange(
|
|
activeAbsence.startDate,
|
|
activeAbsence.endDate,
|
|
)}
|
|
/>
|
|
<DetailLine
|
|
icon={Circle}
|
|
label="Grund"
|
|
value={reasonLabel(activeAbsence.reason)}
|
|
dotColor={reasonColor(activeAbsence.reason)}
|
|
/>
|
|
<DetailLine
|
|
icon={MessageSquareText}
|
|
label="Bemerkung"
|
|
value={activeAbsence.note || "Keine Bemerkung hinterlegt."}
|
|
/>
|
|
<DetailLine icon={UserRound} label="Status" value="Bestätigt" />
|
|
</div>
|
|
</>
|
|
) : null}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function DetailLine({
|
|
icon: Icon,
|
|
label,
|
|
value,
|
|
dotColor,
|
|
}: {
|
|
icon: typeof CalendarDays;
|
|
label: string;
|
|
value: string;
|
|
dotColor?: string;
|
|
}) {
|
|
return (
|
|
<div className="rounded-lg border border-[var(--hairline-soft)] bg-[var(--surface-2)] p-3">
|
|
<p className="flex items-center gap-2 text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
|
|
{dotColor ? (
|
|
<span
|
|
className="h-2 w-2 rounded-full"
|
|
style={{ backgroundColor: dotColor }}
|
|
/>
|
|
) : (
|
|
<Icon className="h-3.5 w-3.5 [stroke-width:1.7]" />
|
|
)}
|
|
{label}
|
|
</p>
|
|
<p className="mt-2 text-sm font-medium text-[var(--ink)]">{value}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function getInitials(name: string) {
|
|
return name
|
|
.split(" ")
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((part) => part[0])
|
|
.join("")
|
|
.toUpperCase();
|
|
}
|
|
|
|
|
|
|
|
export function formatDateRange(startDate: string, endDate: string) {
|
|
const start = new Date(`${startDate}T00:00:00`);
|
|
const end = new Date(`${endDate}T00:00:00`);
|
|
const startLabel = start.toLocaleDateString("de-DE", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
});
|
|
const endLabel = end.toLocaleDateString("de-DE", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
});
|
|
return startDate === endDate ? endLabel : `${startLabel} - ${endLabel}`;
|
|
}
|