Files
kita-planer/src/app/dashboard/families/edit-family-dialog.tsx
T
2026-05-20 22:47:52 +02:00

361 lines
12 KiB
TypeScript

"use client";
import { useMemo, 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 { updateFamilyAction } from "./actions";
type EditableParent = {
id: string;
firstName: string;
lastName: string;
email: string;
};
type EditableChild = {
id: string;
firstName: string;
lastName: string;
};
type DraftParent = EditableParent & {
kind: "existing" | "new";
};
type DraftChild = EditableChild & {
kind: "existing" | "new";
};
type EditableFamily = {
id: string;
name: string;
parents: EditableParent[];
children: EditableChild[];
};
export function EditFamilyDialog({ family }: { family: EditableFamily }) {
const [isPending, startTransition] = useTransition();
const initialParents = useMemo(
() =>
family.parents.map((parent) => ({
...parent,
kind: "existing" as const,
})),
[family.parents],
);
const initialChildren = useMemo(
() =>
family.children.map((child) => ({
...child,
kind: "existing" as const,
})),
[family.children],
);
const [parents, setParents] = useState<DraftParent[]>(initialParents);
const [children, setChildren] = useState<DraftChild[]>(initialChildren);
const [removedChildIds, setRemovedChildIds] = useState<string[]>([]);
function resetState() {
setParents(initialParents);
setChildren(initialChildren);
setRemovedChildIds([]);
}
function addParent() {
if (parents.length >= 2) return;
setParents((current) => [
...current,
{
id: `new-parent-${crypto.randomUUID()}`,
firstName: "",
lastName: "",
email: "",
kind: "new",
},
]);
}
function removeNewParent(parent: DraftParent) {
if (parent.kind !== "new") return;
setParents((current) => current.filter((item) => item.id !== parent.id));
}
function addChild() {
setChildren((current) => [
...current,
{
id: `new-child-${crypto.randomUUID()}`,
firstName: "",
lastName: "",
kind: "new",
},
]);
}
function removeChild(child: DraftChild) {
setChildren((current) => current.filter((item) => item.id !== child.id));
if (child.kind === "existing") {
setRemovedChildIds((current) => [...current, child.id]);
}
}
function handleSubmit(formData: FormData) {
const payload = {
familyId: family.id,
familyName: String(formData.get("familyName") ?? ""),
parents: parents
.filter((parent) => parent.kind === "existing")
.map((parent) => ({
id: parent.id,
firstName: String(formData.get(`parentFirstName_${parent.id}`) ?? ""),
lastName: String(formData.get(`parentLastName_${parent.id}`) ?? ""),
email: String(formData.get(`parentEmail_${parent.id}`) ?? ""),
})),
newParents: parents
.filter((parent) => parent.kind === "new")
.map((parent) => ({
firstName: String(formData.get(`parentFirstName_${parent.id}`) ?? ""),
lastName: String(formData.get(`parentLastName_${parent.id}`) ?? ""),
email: String(formData.get(`parentEmail_${parent.id}`) ?? ""),
})),
children: children
.filter((child) => child.kind === "existing")
.map((child) => ({
id: child.id,
firstName: String(formData.get(`childFirstName_${child.id}`) ?? ""),
lastName: String(formData.get(`childLastName_${child.id}`) ?? ""),
})),
newChildren: children
.filter((child) => child.kind === "new")
.map((child) => ({
firstName: String(formData.get(`childFirstName_${child.id}`) ?? ""),
lastName: String(formData.get(`childLastName_${child.id}`) ?? ""),
})),
removedChildIds,
};
startTransition(async () => {
const result = await updateFamilyAction(payload);
if (result.error) {
toast.error(result.error);
return;
}
toast.success("Familie aktualisiert.");
setRemovedChildIds([]);
});
}
return (
<Dialog onOpenChange={(open) => !open && resetState()}>
<DialogTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 gap-1">
<Pencil className="h-3.5 w-3.5" />
<span className="hidden sm:inline">Details</span>
</Button>
</DialogTrigger>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-2xl">
<form action={handleSubmit}>
<DialogHeader>
<DialogTitle>Familie bearbeiten</DialogTitle>
<DialogDescription>
Aktualisiere Haushalt, Elternteile und Kinder.
</DialogDescription>
</DialogHeader>
<div className="grid gap-6 py-4">
<div className="grid gap-2">
<Label htmlFor={`familyName-${family.id}`}>Familienname</Label>
<Input
id={`familyName-${family.id}`}
name="familyName"
defaultValue={family.name}
required
/>
</div>
<fieldset className="grid gap-3">
<div className="flex items-center justify-between gap-3">
<legend className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Elternteile
</legend>
{parents.length < 2 && (
<Button
type="button"
variant="pill"
size="sm"
className="h-8 gap-1"
onClick={addParent}
disabled={isPending}
>
<Plus className="h-3.5 w-3.5" />
Elternteil hinzufügen
</Button>
)}
</div>
{parents.map((parent, index) => (
<div
key={parent.id}
className="rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] p-3"
>
<div className="mb-3 flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Elternteil {index + 1}
</p>
{parent.kind === "new" && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 gap-1 px-2 text-[var(--danger)] hover:bg-[var(--danger-soft)] hover:text-[var(--danger)]"
onClick={() => removeNewParent(parent)}
disabled={isPending}
>
<Trash2 className="h-3.5 w-3.5" />
Entfernen
</Button>
)}
</div>
<div className="grid gap-3 sm:grid-cols-2">
<Field
id={`parentFirstName-${parent.id}`}
name={`parentFirstName_${parent.id}`}
label="Vorname"
defaultValue={parent.firstName}
/>
<Field
id={`parentLastName-${parent.id}`}
name={`parentLastName_${parent.id}`}
label="Nachname"
defaultValue={parent.lastName}
/>
</div>
<div className="mt-3">
<Field
id={`parentEmail-${parent.id}`}
name={`parentEmail_${parent.id}`}
label="E-Mail-Adresse"
type="email"
defaultValue={parent.email}
/>
</div>
</div>
))}
</fieldset>
<fieldset className="grid gap-3">
<div className="flex items-center justify-between gap-3">
<legend className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Kinder
</legend>
<Button
type="button"
variant="pill"
size="sm"
className="h-8 gap-1"
onClick={addChild}
disabled={isPending}
>
<Plus className="h-3.5 w-3.5" />
Kind hinzufügen
</Button>
</div>
{children.length === 0 ? (
<p className="rounded-lg border border-dashed border-[var(--hairline)] bg-[var(--surface-2)] px-4 py-5 text-sm italic text-[var(--ink-faint)]">
Keine Kinder verknüpft.
</p>
) : (
children.map((child, index) => (
<div
key={child.id}
className="rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] p-3"
>
<div className="mb-3 flex items-center justify-between gap-3">
<p className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Kind {index + 1}
</p>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 gap-1 px-2 text-[var(--danger)] hover:bg-[var(--danger-soft)] hover:text-[var(--danger)]"
onClick={() => removeChild(child)}
disabled={isPending}
>
<Trash2 className="h-3.5 w-3.5" />
Entfernen
</Button>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<Field
id={`childFirstName-${child.id}`}
name={`childFirstName_${child.id}`}
label="Vorname"
defaultValue={child.firstName}
/>
<Field
id={`childLastName-${child.id}`}
name={`childLastName_${child.id}`}
label="Nachname"
defaultValue={child.lastName}
/>
</div>
</div>
))
)}
</fieldset>
</div>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending ? "Speichert..." : "Speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
function Field({
id,
name,
label,
type = "text",
defaultValue,
}: {
id: string;
name: string;
label: string;
type?: string;
defaultValue: string;
}) {
return (
<div className="grid gap-2">
<Label htmlFor={id}>{label}</Label>
<Input
id={id}
name={name}
type={type}
defaultValue={defaultValue}
required
/>
</div>
);
}