Files
kita-planer/src/app/dashboard/families/add-family-dialog.tsx
T

282 lines
9.4 KiB
TypeScript

"use client";
import { useActionState, useState } from "react";
import { Plus, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { addFamilyAction, type AddFamilyState } from "./actions";
const initialState: AddFamilyState = {};
// =====================================================================
// AddFamilyDialog · Client Component
// ---------------------------------------------------------------------
// Verwaltet:
// • Dialog-Open/Close-State
// • Dynamische Kinderliste (min. 1, max. 10)
// • Ein oder zwei Elternteile pro Haushalt
// • useActionState → Server-Action-Fehler anzeigen
// • Bei Erfolg (state.success) Dialog automatisch schließen
// =====================================================================
export function AddFamilyDialog() {
const [open, setOpen] = useState(false);
const [childCount, setChildCount] = useState(1);
const [state, formAction, pending] = useActionState(
addFamilyAction,
initialState,
);
// Dialog schließen, wenn die Action erfolgreich war
if (state.success && open) {
setOpen(false);
}
function handleOpenChange(nextOpen: boolean) {
setOpen(nextOpen);
// State beim Schließen zurücksetzen würde useActionState erfordern —
// stattdessen schließen wir einfach und der State bleibt bis zur nächsten
// Aktion erhalten (unkritisch).
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button size="sm">
<Plus className="h-4 w-4" />
Familie hinzufügen
</Button>
</DialogTrigger>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Familie hinzufügen</DialogTitle>
<DialogDescription>
Lege einen Haushalt mit Elternteilen und Kindern an. Die
Elternteile erhalten Einladungslinks per E-Mail.
</DialogDescription>
</DialogHeader>
<form action={formAction} className="space-y-6">
{/* Anzahl Kinder als verstecktes Feld für die Server Action */}
<input type="hidden" name="childCount" value={childCount} />
<FormField
id="familyName"
name="familyName"
label="Familienname"
placeholder="Familie Schmidt"
error={state.errors?.familyName?.[0]}
/>
{/* ── Elternteile ────────────────────────────────────────── */}
<fieldset className="space-y-4">
<legend className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Elternteil 1
</legend>
<div className="grid grid-cols-2 gap-3">
<FormField
id="parent1FirstName"
name="parent1FirstName"
label="Vorname"
autoComplete="given-name"
error={state.errors?.parent1FirstName?.[0]}
/>
<FormField
id="parent1LastName"
name="parent1LastName"
label="Nachname"
autoComplete="family-name"
error={state.errors?.parent1LastName?.[0]}
/>
</div>
<FormField
id="parent1Email"
name="parent1Email"
type="email"
label="E-Mail-Adresse"
autoComplete="email"
error={state.errors?.parent1Email?.[0]}
/>
</fieldset>
<fieldset className="space-y-4">
<legend>
<span className="text-[11px] uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Elternteil 2
</span>{" "}
<span className="font-normal italic text-[var(--ink-faint)]">
optional
</span>
</legend>
<div className="grid grid-cols-2 gap-3">
<FormField
id="parent2FirstName"
name="parent2FirstName"
label="Vorname"
autoComplete="given-name"
required={false}
error={state.errors?.parent2FirstName?.[0]}
/>
<FormField
id="parent2LastName"
name="parent2LastName"
label="Nachname"
autoComplete="family-name"
required={false}
error={state.errors?.parent2LastName?.[0]}
/>
</div>
<FormField
id="parent2Email"
name="parent2Email"
type="email"
label="E-Mail-Adresse"
autoComplete="email"
required={false}
error={state.errors?.parent2Email?.[0]}
/>
</fieldset>
{/* ── Kinder ─────────────────────────────────────────────── */}
<fieldset className="space-y-3">
<div className="flex items-center justify-between">
<legend className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Kinder ({childCount})
</legend>
{childCount < 10 && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setChildCount((c) => c + 1)}
>
<Plus className="mr-1.5 h-3.5 w-3.5" />
Kind hinzufügen
</Button>
)}
</div>
{Array.from({ length: childCount }).map((_, i) => (
<div
key={i}
className="relative rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] p-4"
>
{childCount > 1 && (
<button
type="button"
onClick={() => {
// Kind entfernen — childCount reduzieren reicht, da die
// Felder nach Index benannt sind und der letzte entfernt wird.
setChildCount((c) => c - 1);
}}
className="absolute right-3 top-3 rounded-md p-1 text-[var(--ink-muted)] transition-colors hover:bg-[var(--danger-soft)] hover:text-[var(--danger)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-2)]"
aria-label={`Kind ${i + 1} entfernen`}
>
<Trash2 className="h-4 w-4" />
</button>
)}
<p className="mb-3 text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
Kind {i + 1}
</p>
<div className="grid grid-cols-2 gap-3">
<FormField
id={`childFirstName_${i}`}
name={`childFirstName_${i}`}
label="Vorname"
/>
<FormField
id={`childLastName_${i}`}
name={`childLastName_${i}`}
label="Nachname"
/>
</div>
</div>
))}
{state.errors?.children?.[0] && (
<p className="text-xs text-[var(--danger)]">
{state.errors.children[0]}
</p>
)}
</fieldset>
{/* Globaler Fehler */}
{state.errors?._form?.[0] && (
<p className="rounded-lg border border-[var(--danger)] bg-[var(--danger-soft)] p-3 text-sm text-[var(--danger)]">
{state.errors._form[0]}
</p>
)}
<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => setOpen(false)}
disabled={pending}
>
Abbrechen
</Button>
<Button type="submit" disabled={pending}>
{pending ? "Wird angelegt…" : "Familie anlegen"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
// ── Hilfskomponente ────────────────────────────────────────────────────
function FormField({
id,
name,
label,
type = "text",
autoComplete,
placeholder,
required = true,
error,
}: {
id: string;
name: string;
label: string;
type?: string;
autoComplete?: string;
placeholder?: string;
required?: boolean;
error?: string;
}) {
return (
<div className="space-y-1.5">
<Label htmlFor={id}>{label}</Label>
<Input
id={id}
name={name}
type={type}
autoComplete={autoComplete}
placeholder={placeholder}
required={required}
aria-invalid={!!error}
/>
{error && <p className="text-xs text-[var(--danger)]">{error}</p>}
</div>
);
}