"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Edit2, Search, Trash2, X } from "lucide-react"; import { useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { useAuth } from "@/components/auth"; import { apiDelete, apiGet, apiSend, Entity, Paginated } from "@/lib/api"; export type FieldOption = { label: string; value: string }; export type FieldConfig = { name: string; label: string; type?: "text" | "email" | "number" | "date" | "textarea" | "select"; required?: boolean; options?: FieldOption[]; }; export type ColumnConfig = { key: keyof T; label: string }; function valueForInput(value: unknown) { return value === null || value === undefined ? "" : String(value); } export function CrudPage({ title, subtitle, endpoint, columns, fields, schema, emptyValues }: { title: string; subtitle: string; endpoint: string; columns: ColumnConfig[]; fields: FieldConfig[]; schema: z.ZodTypeAny; emptyValues: Record; }) { const { token } = useAuth(); const client = useQueryClient(); const [page, setPage] = useState(1); const [search, setSearch] = useState(""); const [editing, setEditing] = useState(null); const [open, setOpen] = useState(false); const pageSize = 10; const queryPath = `${endpoint}?page=${page}&page_size=${pageSize}&search=${encodeURIComponent(search)}`; const form = useForm>({ resolver: zodResolver(schema), defaultValues: emptyValues }); const query = useQuery({ queryKey: [endpoint, page, pageSize, search, token], queryFn: () => apiGet>(queryPath, token ?? ""), enabled: Boolean(token) }); const totalPages = Math.max(1, Math.ceil((query.data?.total ?? 0) / pageSize)); const invalidate = () => client.invalidateQueries({ queryKey: [endpoint] }); const saveMutation = useMutation({ mutationFn: (values: Record) => { const cleaned = Object.fromEntries( Object.entries(values).map(([key, value]) => [key, value === "" ? null : value]) ); return apiSend(editing ? `${endpoint}/${editing.id}` : endpoint, token ?? "", editing ? "PUT" : "POST", cleaned); }, onSuccess: () => { setOpen(false); setEditing(null); form.reset(emptyValues); invalidate(); } }); const deleteMutation = useMutation({ mutationFn: (item: T) => apiDelete(`${endpoint}/${item.id}`, token ?? ""), onSuccess: invalidate }); const rows = useMemo(() => query.data?.items ?? [], [query.data]); function startCreate() { setEditing(null); form.reset(emptyValues); setOpen(true); } function startEdit(item: T) { setEditing(item); form.reset(Object.fromEntries(Object.keys(emptyValues).map((key) => [key, valueForInput(item[key as keyof T])]))); setOpen(true); } return (

{title}

{subtitle}

{ setPage(1); setSearch(event.target.value); }} placeholder="Suchen" className="h-9 flex-1 bg-transparent outline-none" />
{columns.map((column) => )} {rows.map((item) => ( {columns.map((column) => )} ))} {!query.isLoading && rows.length === 0 && ( )}
{column.label}Aktionen
{valueForInput(item[column.key])}
Keine Datensaetze gefunden.
{query.data?.total ?? 0} Datensaetze
Seite {page} von {totalPages}
{open && (
saveMutation.mutate(values))} className="max-h-[92vh] w-full max-w-3xl overflow-y-auto rounded-lg bg-surface p-6 shadow-soft">

{editing ? "Bearbeiten" : "Neu"}

{fields.map((field) => (