355 lines
10 KiB
TypeScript
355 lines
10 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 { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
|
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 [childToDelete, setChildToDelete] = useState<MyChild | null>(null);
|
|
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) {
|
|
startTransition(async () => {
|
|
const result = await deleteMyChild(child.id);
|
|
if (result.error) {
|
|
toast.error(result.error);
|
|
return;
|
|
}
|
|
|
|
toast.success("Kind entfernt.");
|
|
setChildToDelete(null);
|
|
});
|
|
}
|
|
|
|
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={() => setChildToDelete(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>
|
|
|
|
<ConfirmDialog
|
|
open={!!childToDelete}
|
|
onOpenChange={(open) => {
|
|
if (!open) setChildToDelete(null);
|
|
}}
|
|
title="Kind entfernen?"
|
|
description={
|
|
childToDelete
|
|
? `${childToDelete.firstName} ${childToDelete.lastName} wird aus deinem Haushalt entfernt.`
|
|
: "Dieses Kind wird aus deinem Haushalt entfernt."
|
|
}
|
|
confirmLabel="Entfernen"
|
|
pendingLabel="Wird entfernt..."
|
|
isPending={isPending}
|
|
onConfirm={() => {
|
|
if (childToDelete) handleDelete(childToDelete);
|
|
}}
|
|
/>
|
|
</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();
|
|
}
|