361 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|