Files
Speisekarte/components/CodeListManager.tsx
T
florian.wilde 51fdb93e97 Automatisch-übersetzen-Button nur bei konfiguriertem DEEPL_API_KEY
Statt den Button immer anzuzeigen und erst nach Klick auf die
fehlende Konfiguration hinzuweisen, wird er serverseitig anhand von
process.env.DEEPL_API_KEY ein-/ausgeblendet (Gerichte, Allergene,
Zusatzstoffe). Die dadurch überflüssige "translateUnavailable"-
Meldung wurde aus allen drei Sprachen entfernt.
2026-07-18 08:14:49 +02:00

265 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 { formatMessage } from "@/lib/i18n/shared";
import { localized } from "@/lib/types";
import type { CodeItem } from "@/lib/types";
import { useDict, useLocale } from "@/components/i18n/LocaleProvider";
export default function CodeListManager({
table,
translateEnabled,
}: {
table: "allergens" | "additives";
translateEnabled: boolean;
}) {
const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
const title =
table === "allergens" ? dict.codeList.allergensTitle : dict.codeList.additivesTitle;
const codeLabel =
table === "allergens"
? dict.codeList.allergenCodeLabel
: dict.codeList.additiveCodeLabel;
const [items, setItems] = useState<CodeItem[]>([]);
const [editing, setEditing] = useState<{
original: string | null; // code vor der Bearbeitung, null = neu
code: string;
name: string;
nameEn: string;
namePl: string;
} | null>(null);
const [error, setError] = useState<string | null>(null);
const [translateState, setTranslateState] = useState<"idle" | "busy" | "done">(
"idle"
);
const load = useCallback(async () => {
const { data, error } = await supabase.from(table).select("*").order("sort");
if (error) setError(formatMessage(dict.codeList.loadError, { message: error.message }));
else setItems(data as CodeItem[]);
}, [supabase, table, dict]);
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;
const payload = {
code,
name,
name_en: editing.nameEn.trim() || null,
name_pl: editing.namePl.trim() || null,
};
let dbError;
if (editing.original) {
({ error: dbError } = await supabase
.from(table)
.update(payload)
.eq("code", editing.original));
} else {
const maxSort = items.reduce((m, i) => Math.max(m, i.sort), 0);
({ error: dbError } = await supabase
.from(table)
.insert({ ...payload, sort: maxSort + 1 }));
}
if (dbError) {
setError(
/duplicate key/i.test(dbError.message)
? formatMessage(dict.codeList.duplicateCode, { code })
: formatMessage(dict.codeList.saveError, { message: dbError.message })
);
return;
}
setEditing(null);
await load();
}
async function remove(item: CodeItem) {
if (
!window.confirm(
formatMessage(dict.codeList.confirmDelete, { code: item.code, name: item.name })
)
)
return;
const { error } = await supabase.from(table).delete().eq("code", item.code);
if (error)
setError(formatMessage(dict.codeList.deleteError, { message: error.message }));
await load();
}
async function autoTranslate() {
if (!editing || !editing.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,
id: editing.original,
text: editing.name.trim(),
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error ?? `Fehler (${res.status})`);
setEditing((prev) =>
prev ? { ...prev, nameEn: data.en ?? prev.nameEn, namePl: data.pl ?? prev.namePl } : prev
);
setTranslateState("done");
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setTranslateState("idle");
}
}
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: "", nameEn: "", namePl: "" });
setTranslateState("idle");
}}
>
{dict.common.add}
</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">{dict.codeList.nameLabel}</label>
<input
className="input"
value={editing.name}
onChange={(e) => setEditing({ ...editing, name: e.target.value })}
required
/>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div>
<label className="label">{dict.dishManager.nameEnLabel}</label>
<input
className="input"
value={editing.nameEn}
onChange={(e) => setEditing({ ...editing, nameEn: e.target.value })}
/>
</div>
<div>
<label className="label">{dict.dishManager.namePlLabel}</label>
<input
className="input"
value={editing.namePl}
onChange={(e) => setEditing({ ...editing, namePl: e.target.value })}
/>
</div>
</div>
{translateEnabled && (
<div>
<button
type="button"
className="btn btn-neutral"
onClick={autoTranslate}
disabled={translateState === "busy" || !editing.name.trim()}
>
{translateState === "busy"
? dict.dishManager.translating
: dict.dishManager.autoTranslate}
</button>
{translateState === "done" && (
<span className="ml-2 text-sm text-green-700">
{dict.dishManager.translated}
</span>
)}
</div>
)}
<div className="flex gap-2">
<button type="submit" className="btn btn-add">
{dict.common.save}
</button>
<button
type="button"
className="btn btn-neutral"
onClick={() => setEditing(null)}
>
{dict.common.cancel}
</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> –{" "}
{localized(item, locale)}
</span>
<button
type="button"
className="btn btn-primary"
onClick={() => {
setEditing({
original: item.code,
code: item.code,
name: item.name,
nameEn: item.name_en ?? "",
namePl: item.name_pl ?? "",
});
setTranslateState("idle");
}}
>
{dict.common.edit}
</button>
<button
type="button"
className="btn btn-del"
onClick={() => remove(item)}
>
{dict.common.delete}
</button>
</li>
))}
{items.length === 0 && (
<li className="py-2 text-gray-500 text-sm">{dict.codeList.empty}</li>
)}
</ul>
</section>
);
}