"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { dishCodes, formatPrice } from "@/lib/menu"; import { formatMessage } from "@/lib/i18n/shared"; import { localized } from "@/lib/types"; import type { CodeItem, Dish } from "@/lib/types"; import ChipPicker from "@/components/ChipPicker"; import { useDict, useLanguages, useLocale } from "@/components/i18n/LocaleProvider"; interface DishForm { id: string | null; // null = neues Gericht name: string; translations: Record; price: string; allergens: string[]; additives: string[]; } const EMPTY_FORM: DishForm = { id: null, name: "", translations: {}, price: "", allergens: [], additives: [], }; export default function DishManager({ translateEnabled, }: { translateEnabled: boolean; }) { const dict = useDict(); const locale = useLocale(); const languages = useLanguages().filter((l) => l.code !== "de"); const [dishes, setDishes] = useState([]); const [allergens, setAllergens] = useState([]); const [additives, setAdditives] = useState([]); const [form, setForm] = useState(null); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [ioBusy, setIoBusy] = useState(false); const [loading, setLoading] = useState(true); const [selected, setSelected] = useState>(new Set()); const [bulkBusy, setBulkBusy] = useState(false); const [search, setSearch] = useState(""); const [labelingDetailed, setLabelingDetailed] = useState(true); const [translateState, setTranslateState] = useState<"idle" | "busy" | "done">( "idle" ); // Live-Suche: filtert die Liste bereits während des Tippens const visible = useMemo(() => { const q = search.trim().toLowerCase(); return q ? dishes.filter((d) => d.name.toLowerCase().includes(q)) : dishes; }, [dishes, search]); const load = useCallback(async () => { const [dishesRes, allergensRes, additivesRes, settingsRes] = await Promise.all([ fetch("/api/dishes"), fetch("/api/allergens"), fetch("/api/additives"), fetch("/api/settings"), ]); if (!dishesRes.ok || !allergensRes.ok || !additivesRes.ok) { setError(formatMessage(dict.dishManager.loadError, { message: "load failed" })); } else { setDishes(await dishesRes.json()); setAllergens(await allergensRes.json()); setAdditives(await additivesRes.json()); } if (settingsRes.ok) { const settings = await settingsRes.json(); setLabelingDetailed((settings?.labeling_mode ?? "detailed") !== "notice"); } setLoading(false); }, [dict]); useEffect(() => { load(); }, [load]); async function save(e: React.FormEvent) { e.preventDefault(); if (!form) return; setError(null); const price = Number.parseFloat(form.price.replace(",", ".")); if (Number.isNaN(price) || price < 0) { setError(dict.dishManager.invalidPrice); return; } const payload = { name: form.name.trim(), price, allergens: form.allergens, additives: form.additives, translations: form.translations, }; const res = form.id ? await fetch(`/api/dishes/${form.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) : await fetch("/api/dishes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) { setError(formatMessage(dict.dishManager.saveError, { message: String(res.status) })); return; } setForm(null); await load(); } async function remove(dish: Dish) { if ( !window.confirm( formatMessage(dict.dishManager.confirmDelete, { name: dish.name }) ) ) return; const res = await fetch(`/api/dishes/${dish.id}`, { method: "DELETE" }); if (!res.ok) setError(formatMessage(dict.dishManager.deleteError, { message: String(res.status) })); setSelected((prev) => { const next = new Set(prev); next.delete(dish.id); return next; }); await load(); } function toggleOne(id: string) { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } // "Alle auswählen" bezieht sich auf die aktuell gefilterte Liste function toggleAll() { setSelected((prev) => { const next = new Set(prev); const everyVisibleSelected = visible.every((d) => next.has(d.id)); visible.forEach((d) => everyVisibleSelected ? next.delete(d.id) : next.add(d.id) ); return next; }); } async function removeSelected() { if (selected.size === 0) return; if ( !window.confirm( formatMessage(dict.dishManager.confirmDeleteSelected, { count: selected.size, }) ) ) return; setBulkBusy(true); setError(null); const res = await fetch("/api/dishes/bulk-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: Array.from(selected) }), }); if (!res.ok) setError(formatMessage(dict.dishManager.deleteError, { message: String(res.status) })); setSelected(new Set()); setBulkBusy(false); await load(); } function toggleAllergen(code: string) { if (!form) return; setForm({ ...form, allergens: form.allergens.includes(code) ? form.allergens.filter((c) => c !== code) : [...form.allergens, code], }); } function toggleAdditive(code: string) { if (!form) return; setForm({ ...form, additives: form.additives.includes(code) ? form.additives.filter((c) => c !== code) : [...form.additives, code], }); } async function autoTranslate() { if (!form || !form.name.trim()) return; setTranslateState("busy"); setError(null); try { const res = await fetch("/api/translate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ table: "dishes", id: form.id, text: form.name.trim(), }), }); const data = await res.json(); if (!res.ok) throw new Error(data?.error ?? `Fehler (${res.status})`); setForm((prev) => prev ? { ...prev, translations: { ...prev.translations, ...(data.translations ?? {}) } } : prev ); setTranslateState("done"); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setTranslateState("idle"); } } async function exportDishes() { setError(null); setNotice(null); const res = await fetch("/api/dishes/export"); if (!res.ok) { setError(formatMessage(dict.dishManager.exportError, { message: String(res.status) })); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "gerichte.csv"; a.click(); URL.revokeObjectURL(url); } async function importDishes(e: React.ChangeEvent) { const file = e.target.files?.[0]; e.target.value = ""; if (!file) return; setError(null); setNotice(null); setIoBusy(true); const fd = new FormData(); fd.append("file", file); const res = await fetch("/api/dishes/import", { method: "POST", body: fd }); setIoBusy(false); if (!res.ok) { setError(formatMessage(dict.dishManager.importError, { message: String(res.status) })); return; } const data = (await res.json()) as { created: number; updated: number; skipped: { row: number; reason: string }[]; }; let msg = formatMessage(dict.dishManager.importSuccess, { created: data.created, updated: data.updated, }); if (data.skipped.length > 0) { const reasons = data.skipped .map((s) => formatMessage(dict.dishManager.importSkippedRow, s)) .join("; "); msg += " " + formatMessage(dict.dishManager.importSkipped, { count: data.skipped.length, reasons }); } setNotice(msg); await load(); } const allSelected = visible.length > 0 && visible.every((d) => selected.has(d.id)); return (

{dict.dishManager.title}

{error && (

{error}

)} {notice &&

{notice}

} {form && (

{form.id ? dict.dishManager.editDish : dict.dishManager.newDish}

setForm({ ...form, name: e.target.value })} required />
setForm({ ...form, price: e.target.value })} required />
{languages.length > 0 && (
{languages.map((lang) => (
setForm({ ...form, translations: { ...form.translations, [lang.code]: e.target.value }, }) } />
))}
)} {translateEnabled && languages.length > 0 && (
{translateState === "done" && ( {dict.dishManager.translated} )}
)} {labelingDetailed ? ( <> ) : (

{dict.dishManager.labelingDisabledHint}

)}
)} {loading ? (

{dict.common.loading}

) : ( <> {dishes.length > 0 && ( setSearch(e.target.value)} aria-label={dict.dishManager.searchPlaceholder} /> )} {dishes.length > 0 && (
{formatMessage(dict.dishManager.selectedCount, { count: selected.size })}
)}
    {visible.map((dish) => { const codes = dishCodes(dish); return (
  • toggleOne(dish.id)} aria-label={dish.name} /> {localized(dish, locale)} {codes && {codes}}{" "} – {formatPrice(dish.price)}
  • ); })} {dishes.length === 0 && (
  • {dict.dishManager.empty}
  • )} {dishes.length > 0 && visible.length === 0 && (
  • {formatMessage(dict.dishManager.noResults, { query: search })}
  • )}
)}
); }