169 lines
4.8 KiB
TypeScript
169 lines
4.8 KiB
TypeScript
"use client";
|
||
|
||
// Pflege der globalen Allergen-/Zusatzstofflisten, die unterhalb
|
||
// der öffentlichen Speisekarte angezeigt werden.
|
||
|
||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
import { createClient } from "@/lib/supabase/client";
|
||
import type { CodeItem } from "@/lib/types";
|
||
|
||
export default function CodeListManager({
|
||
table,
|
||
title,
|
||
codeLabel,
|
||
}: {
|
||
table: "allergens" | "additives";
|
||
title: string;
|
||
codeLabel: string;
|
||
}) {
|
||
const supabase = useMemo(() => createClient(), []);
|
||
const [items, setItems] = useState<CodeItem[]>([]);
|
||
const [editing, setEditing] = useState<{
|
||
original: string | null; // code vor der Bearbeitung, null = neu
|
||
code: string;
|
||
name: string;
|
||
} | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const load = useCallback(async () => {
|
||
const { data, error } = await supabase.from(table).select("*").order("sort");
|
||
if (error) setError(`Laden fehlgeschlagen: ${error.message}`);
|
||
else setItems(data as CodeItem[]);
|
||
}, [supabase, table]);
|
||
|
||
useEffect(() => {
|
||
load();
|
||
}, [load]);
|
||
|
||
async function save(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
if (!editing) return;
|
||
setError(null);
|
||
const code = editing.code.trim();
|
||
const name = editing.name.trim();
|
||
if (!code || !name) return;
|
||
|
||
let dbError;
|
||
if (editing.original) {
|
||
({ error: dbError } = await supabase
|
||
.from(table)
|
||
.update({ code, name })
|
||
.eq("code", editing.original));
|
||
} else {
|
||
const maxSort = items.reduce((m, i) => Math.max(m, i.sort), 0);
|
||
({ error: dbError } = await supabase
|
||
.from(table)
|
||
.insert({ code, name, sort: maxSort + 1 }));
|
||
}
|
||
if (dbError) {
|
||
setError(
|
||
/duplicate key/i.test(dbError.message)
|
||
? `Das Kürzel „${code}“ existiert bereits.`
|
||
: `Speichern fehlgeschlagen: ${dbError.message}`
|
||
);
|
||
return;
|
||
}
|
||
setEditing(null);
|
||
await load();
|
||
}
|
||
|
||
async function remove(item: CodeItem) {
|
||
if (!window.confirm(`„${item.code} – ${item.name}“ wirklich löschen?`))
|
||
return;
|
||
const { error } = await supabase.from(table).delete().eq("code", item.code);
|
||
if (error) setError(`Löschen fehlgeschlagen: ${error.message}`);
|
||
await load();
|
||
}
|
||
|
||
return (
|
||
<section className="card">
|
||
<div className="flex items-center gap-3 mb-3">
|
||
<h2 className="text-lg font-bold text-[var(--c-primary)] mr-auto">
|
||
{title}
|
||
</h2>
|
||
<button
|
||
type="button"
|
||
className="btn btn-add"
|
||
onClick={() => setEditing({ original: null, code: "", name: "" })}
|
||
>
|
||
Hinzufügen
|
||
</button>
|
||
</div>
|
||
|
||
{error && (
|
||
<p className="mb-2 text-sm text-[var(--c-del)]" role="alert">
|
||
{error}
|
||
</p>
|
||
)}
|
||
|
||
{editing && (
|
||
<form onSubmit={save} className="mb-3 space-y-2 border-b border-gray-200 pb-3">
|
||
<div className="max-w-36">
|
||
<label className="label">{codeLabel}</label>
|
||
<input
|
||
className="input"
|
||
value={editing.code}
|
||
onChange={(e) => setEditing({ ...editing, code: e.target.value })}
|
||
required
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="label">Bezeichnung</label>
|
||
<input
|
||
className="input"
|
||
value={editing.name}
|
||
onChange={(e) => setEditing({ ...editing, name: e.target.value })}
|
||
required
|
||
/>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button type="submit" className="btn btn-add">
|
||
Speichern
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-neutral"
|
||
onClick={() => setEditing(null)}
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</form>
|
||
)}
|
||
|
||
<ul className="divide-y divide-gray-100">
|
||
{items.map((item) => (
|
||
<li key={item.code} className="flex items-center gap-2 py-1.5 text-sm">
|
||
<span className="mr-auto">
|
||
<span className="font-semibold">{item.code}</span> – {item.name}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary"
|
||
onClick={() =>
|
||
setEditing({
|
||
original: item.code,
|
||
code: item.code,
|
||
name: item.name,
|
||
})
|
||
}
|
||
>
|
||
Bearbeiten
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-del"
|
||
onClick={() => remove(item)}
|
||
>
|
||
Löschen
|
||
</button>
|
||
</li>
|
||
))}
|
||
{items.length === 0 && (
|
||
<li className="py-2 text-gray-500 text-sm">Keine Einträge.</li>
|
||
)}
|
||
</ul>
|
||
</section>
|
||
);
|
||
}
|