continued the kita-planer

This commit is contained in:
t.indorf
2026-05-08 14:32:14 +02:00
parent b686e714ff
commit 7aff691803
85 changed files with 9434 additions and 588 deletions
@@ -0,0 +1,350 @@
"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-4 w-4" />
<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-sm font-semibold">Elternteile</legend>
{parents.length < 2 && (
<Button
type="button"
variant="outline"
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-md border p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<p className="text-xs font-medium text-muted-foreground">
Elternteil {index + 1}
</p>
{parent.kind === "new" && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 gap-1 px-2 text-destructive hover:text-destructive"
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-sm font-semibold">Kinder</legend>
<Button
type="button"
variant="outline"
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="text-sm text-muted-foreground">
Keine Kinder verknüpft.
</p>
) : (
children.map((child, index) => (
<div key={child.id} className="rounded-md border p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<p className="text-xs font-medium text-muted-foreground">
Kind {index + 1}
</p>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 gap-1 px-2 text-destructive hover:text-destructive"
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>
);
}