Files
kita-planer/src/components/absence-management-actions.tsx
T

253 lines
8.3 KiB
TypeScript

"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Pencil, Undo2 } from "lucide-react";
import { AbsenceReason } from "@prisma/client";
import { toast } from "sonner";
import { deleteAbsence, updateAbsence } from "@/actions/absences";
import { absenceDate, canEditOwnAbsence } from "@/lib/absence-policy";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export type ManagedAbsence = {
id: string;
childName: string;
startDate: string;
endDate: string;
reason: AbsenceReason;
note: string | null;
updatedAt: string;
};
export function AbsenceManagementActions({
absence,
today,
team = false,
onChanged,
}: {
absence: ManagedAbsence;
today: string;
team?: boolean;
onChanged?: () => void;
}) {
const [mode, setMode] = useState<"edit" | "withdraw" | null>(null);
const [pending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [startDate, setStartDate] = useState(absence.startDate);
const [endDate, setEndDate] = useState(absence.endDate);
const router = useRouter();
if (!team && !canEditOwnAbsence(absence.startDate, today))
return (
<p className="mt-2 text-xs text-[var(--ink-muted)]">
Bereits begonnene Meldung: Änderungen über die Kita.
</p>
);
const tomorrow = new Date(absenceDate(today).getTime() + 86_400_000)
.toISOString()
.slice(0, 10);
function open(next: "edit" | "withdraw") {
setStartDate(absence.startDate);
setEndDate(absence.endDate);
setError(null);
setMode(next);
}
function submit(data?: FormData) {
startTransition(async () => {
try {
const identity = { id: absence.id, updatedAt: absence.updatedAt };
const result =
mode === "withdraw"
? await deleteAbsence(identity)
: await updateAbsence({
...identity,
startDate,
endDate,
reason: String(data?.get("reason") ?? ""),
note: String(data?.get("note") ?? ""),
});
if (result.error) {
setError(result.error);
return;
}
toast.success(
mode === "withdraw"
? "Abwesenheitsmeldung zurückgenommen."
: "Abwesenheitsmeldung aktualisiert.",
);
setMode(null);
onChanged?.();
} catch {
setError(
"Die Meldung konnte nicht gespeichert werden. Bitte erneut versuchen.",
);
}
});
}
return (
<div className="mt-2 flex flex-wrap gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => open("edit")}
aria-label={`Abwesenheit bearbeiten: ${absence.childName}`}
>
<Pencil className="h-3.5 w-3.5" /> Bearbeiten
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => open("withdraw")}
aria-label={`Abwesenheit zurücknehmen: ${absence.childName}`}
>
<Undo2 className="h-3.5 w-3.5" /> Zurücknehmen
</Button>
<Dialog
open={mode !== null}
onOpenChange={(isOpen) => {
if (!isOpen && !pending) setMode(null);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
{mode === "withdraw"
? "Abwesenheit zurücknehmen"
: "Abwesenheit bearbeiten"}
</DialogTitle>
<DialogDescription>
{absence.childName}
{mode === "withdraw"
? ": Diese Meldung wird aus der Abwesenheitsübersicht entfernt."
: ": Passe den Zeitraum, den Grund oder die Bemerkung an."}
{!team && mode === "edit"
? " Änderungen sind nur für zukünftige Tage möglich."
: ""}
</DialogDescription>
</DialogHeader>
<form
onSubmit={(event) => {
event.preventDefault();
submit(new FormData(event.currentTarget));
}}
>
{mode === "edit" && (
<fieldset disabled={pending} className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor={`absence-start-${absence.id}`}>Von</Label>
<Input
id={`absence-start-${absence.id}`}
type="date"
required
min={team ? undefined : tomorrow}
value={startDate}
onChange={(event) => {
const value = event.target.value;
setStartDate(value);
if (value > endDate) setEndDate(value);
}}
/>
</div>
<div className="grid gap-2">
<Label htmlFor={`absence-end-${absence.id}`}>Bis</Label>
<Input
id={`absence-end-${absence.id}`}
type="date"
required
min={startDate}
value={endDate}
onChange={(event) => setEndDate(event.target.value)}
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor={`absence-reason-${absence.id}`}>Grund</Label>
<select
id={`absence-reason-${absence.id}`}
name="reason"
defaultValue={absence.reason}
className="h-10 rounded-lg border border-[var(--hairline)] bg-[var(--surface)] px-3 text-sm"
required
>
<option value={AbsenceReason.ILLNESS}>Krank</option>
<option value={AbsenceReason.VACATION}>Urlaub</option>
<option value={AbsenceReason.OTHER}>Sonstiges</option>
</select>
</div>
<div className="grid gap-2">
<Label htmlFor={`absence-note-${absence.id}`}>
Bemerkung (optional)
</Label>
<Textarea
id={`absence-note-${absence.id}`}
name="note"
maxLength={500}
defaultValue={absence.note ?? ""}
rows={3}
/>
</div>
</fieldset>
)}
{error && (
<div
role="alert"
className="my-4 rounded-lg bg-[var(--danger-soft)] p-3 text-sm text-[var(--danger)]"
>
{error}
{error.includes("aktualisieren") && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setMode(null);
onChanged?.();
router.refresh();
}}
>
Ansicht aktualisieren
</Button>
)}
</div>
)}
<DialogFooter className="mt-4">
<Button
type="button"
variant="ghost"
disabled={pending}
onClick={() => setMode(null)}
>
Abbrechen
</Button>
<Button
type="submit"
disabled={pending}
variant={mode === "withdraw" ? "destructive" : "default"}
>
{pending
? "Wird gespeichert…"
: mode === "withdraw"
? "Meldung zurücknehmen"
: "Änderungen speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}