Files
kita-planer/src/app/dashboard/profil/_components/my-children-manager.tsx
T
2026-05-20 22:47:52 +02:00

338 lines
9.6 KiB
TypeScript

"use client";
import { type FormEvent, useState, useTransition } from "react";
import { Pencil, Plus, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { getChildBirthdateRange } from "@/lib/child-birthdate";
import { createMyChild, deleteMyChild, updateMyChild } from "../actions";
export type MyChild = {
id: string;
firstName: string;
lastName: string;
dateOfBirth: string | null;
};
type ChildPayload = {
firstName: string;
lastName: string;
dateOfBirth: string;
};
const emptyChildPayload: ChildPayload = {
firstName: "",
lastName: "",
dateOfBirth: "",
};
export function MyChildrenManager({
items,
canManage,
}: {
items: MyChild[];
canManage: boolean;
}) {
const [createOpen, setCreateOpen] = useState(false);
const [createPayload, setCreatePayload] =
useState<ChildPayload>(emptyChildPayload);
const [editingChild, setEditingChild] = useState<MyChild | null>(null);
const [editPayload, setEditPayload] =
useState<ChildPayload>(emptyChildPayload);
const [isPending, startTransition] = useTransition();
function openEditor(child: MyChild) {
setEditingChild(child);
setEditPayload(payloadFromChild(child));
}
function handleCreate(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
startTransition(async () => {
const result = await createMyChild(createPayload);
if (result.error) {
toast.error(result.error);
return;
}
toast.success("Kind angelegt.");
setCreatePayload(emptyChildPayload);
setCreateOpen(false);
});
}
function handleUpdate(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!editingChild) return;
startTransition(async () => {
const result = await updateMyChild(editingChild.id, editPayload);
if (result.error) {
toast.error(result.error);
return;
}
toast.success("Kind aktualisiert.");
setEditingChild(null);
});
}
function handleDelete(child: MyChild) {
const confirmed = confirm(
`${child.firstName} ${child.lastName} wirklich entfernen?`,
);
if (!confirmed) return;
startTransition(async () => {
const result = await deleteMyChild(child.id);
if (result.error) {
toast.error(result.error);
return;
}
toast.success("Kind entfernt.");
});
}
return (
<div className="space-y-3">
{items.length === 0 ? (
<p className="text-sm italic text-[var(--ink-faint)]">
Keine Kinder verknüpft.
</p>
) : (
<div className="flex flex-col gap-2">
{items.map((child) => (
<div
key={child.id}
className="flex items-center justify-between gap-3 rounded-lg bg-[var(--surface-2)] p-[10px_12px]"
>
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-[var(--accent-soft)] text-[11px] font-semibold text-[var(--accent-deep)]">
{getInitials(child)}
</div>
<div className="min-w-0">
<p className="truncate text-sm font-semibold text-[var(--ink)]">
{child.firstName} {child.lastName}
</p>
{child.dateOfBirth ? (
<p className="text-xs text-[var(--ink-soft)]">
Geboren am{" "}
{new Date(child.dateOfBirth).toLocaleDateString("de-DE")}
</p>
) : (
<p className="text-xs italic text-[var(--ink-faint)]">
Geburtsdatum fehlt
</p>
)}
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => openEditor(child)}
disabled={isPending || !canManage}
aria-label="Kind bearbeiten"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-[var(--danger)] hover:bg-[var(--danger-soft)] hover:text-[var(--danger)]"
onClick={() => handleDelete(child)}
disabled={isPending || !canManage}
aria-label="Kind löschen"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
{canManage ? (
<Dialog
open={createOpen}
onOpenChange={(open) => {
setCreateOpen(open);
if (!open) setCreatePayload(emptyChildPayload);
}}
>
<DialogTrigger asChild>
<Button type="button" variant="pill" size="sm" className="w-full">
<Plus className="h-4 w-4" />
Kind hinzufügen
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleCreate}>
<DialogHeader>
<DialogTitle>Kind hinzufügen</DialogTitle>
<DialogDescription>
Dieses Kind wird mit deinem Haushalt verknüpft.
</DialogDescription>
</DialogHeader>
<ChildFields
idPrefix="create-child"
value={createPayload}
onChange={setCreatePayload}
/>
<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => setCreateOpen(false)}
disabled={isPending}
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
Speichern
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
) : (
<p className="text-xs italic text-[var(--ink-faint)]">
Dein Account ist noch keinem Haushalt zugeordnet.
</p>
)}
<Dialog
open={!!editingChild}
onOpenChange={(open) => !open && setEditingChild(null)}
>
<DialogContent>
<form onSubmit={handleUpdate}>
<DialogHeader>
<DialogTitle>Kind bearbeiten</DialogTitle>
</DialogHeader>
{editingChild && (
<ChildFields
idPrefix="edit-child"
value={editPayload}
onChange={setEditPayload}
/>
)}
<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => setEditingChild(null)}
disabled={isPending}
>
Abbrechen
</Button>
<Button type="submit" disabled={isPending}>
Aktualisieren
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}
function ChildFields({
idPrefix,
value,
onChange,
}: {
idPrefix: string;
value: ChildPayload;
onChange: (value: ChildPayload) => void;
}) {
const birthdateRange = getChildBirthdateRange();
return (
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label
htmlFor={`${idPrefix}-firstName`}
className="text-xs font-semibold text-[var(--ink-soft)]"
>
Vorname
</Label>
<Input
id={`${idPrefix}-firstName`}
name="firstName"
value={value.firstName}
onChange={(event) =>
onChange({ ...value, firstName: event.target.value })
}
required
/>
</div>
<div className="grid gap-2">
<Label
htmlFor={`${idPrefix}-lastName`}
className="text-xs font-semibold text-[var(--ink-soft)]"
>
Nachname
</Label>
<Input
id={`${idPrefix}-lastName`}
name="lastName"
value={value.lastName}
onChange={(event) =>
onChange({ ...value, lastName: event.target.value })
}
required
/>
</div>
<div className="grid gap-2">
<Label
htmlFor={`${idPrefix}-dateOfBirth`}
className="text-xs font-semibold text-[var(--ink-soft)]"
>
Geburtsdatum
</Label>
<Input
id={`${idPrefix}-dateOfBirth`}
name="dateOfBirth"
type="date"
min={birthdateRange.min}
max={birthdateRange.max}
value={value.dateOfBirth}
onChange={(event) =>
onChange({ ...value, dateOfBirth: event.target.value })
}
/>
</div>
</div>
);
}
function payloadFromChild(child: MyChild): ChildPayload {
return {
firstName: child.firstName,
lastName: child.lastName,
dateOfBirth: child.dateOfBirth ?? "",
};
}
function getInitials(child: MyChild) {
return `${child.firstName[0] ?? ""}${child.lastName[0] ?? ""}`.toUpperCase();
}