Allergene/Zusatzstoffe je Gericht auswählbar, klein angezeigt

Chip-Auswahl im Gericht-Formular (Klick zum An-/Abwählen) statt
Checkbox-Liste; Kürzel erscheinen hochgestellt neben dem Gerichtnamen
in Speisekarte, mobiler Ansicht und Wochenplan-Editor. Editor bekommt
außerdem einen Heute-Button.
This commit is contained in:
2026-07-18 00:46:39 +02:00
parent 1db89a54bf
commit 675503e1f4
4 changed files with 202 additions and 52 deletions
+107 -45
View File
@@ -2,20 +2,31 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { formatPrice } from "@/lib/menu";
import type { Dish } from "@/lib/types";
import { dishCodes, formatPrice } from "@/lib/menu";
import type { CodeItem, Dish } from "@/lib/types";
import ChipPicker from "@/components/ChipPicker";
interface DishForm {
id: string | null; // null = neues Gericht
name: string;
price: string;
allergens: string[];
additives: string[];
}
const EMPTY_FORM: DishForm = { id: null, name: "", price: "" };
const EMPTY_FORM: DishForm = {
id: null,
name: "",
price: "",
allergens: [],
additives: [],
};
export default function DishManager() {
const supabase = useMemo(() => createClient(), []);
const [dishes, setDishes] = useState<Dish[]>([]);
const [allergens, setAllergens] = useState<CodeItem[]>([]);
const [additives, setAdditives] = useState<CodeItem[]>([]);
const [form, setForm] = useState<DishForm | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
@@ -30,12 +41,18 @@ export default function DishManager() {
}, [dishes, search]);
const load = useCallback(async () => {
const { data, error } = await supabase
.from("dishes")
.select("*")
.order("name");
if (error) setError(`Laden fehlgeschlagen: ${error.message}`);
else setDishes(data as Dish[]);
const [dishesRes, allergensRes, additivesRes] = await Promise.all([
supabase.from("dishes").select("*").order("name"),
supabase.from("allergens").select("*").order("sort"),
supabase.from("additives").select("*").order("sort"),
]);
const problem = dishesRes.error ?? allergensRes.error ?? additivesRes.error;
if (problem) setError(`Laden fehlgeschlagen: ${problem.message}`);
else {
setDishes(dishesRes.data as Dish[]);
setAllergens(allergensRes.data as CodeItem[]);
setAdditives(additivesRes.data as CodeItem[]);
}
setLoading(false);
}, [supabase]);
@@ -52,7 +69,12 @@ export default function DishManager() {
setError("Bitte einen gültigen Preis angeben, z. B. 3,50");
return;
}
const payload = { name: form.name.trim(), price };
const payload = {
name: form.name.trim(),
price,
allergens: form.allergens,
additives: form.additives,
};
const { error } = form.id
? await supabase.from("dishes").update(payload).eq("id", form.id)
: await supabase.from("dishes").insert(payload);
@@ -122,6 +144,26 @@ export default function DishManager() {
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],
});
}
const allSelected =
visible.length > 0 && visible.every((d) => selected.has(d.id));
@@ -147,7 +189,7 @@ export default function DishManager() {
)}
{form && (
<form onSubmit={save} className="card mb-4 space-y-3">
<form onSubmit={save} className="card mb-4 space-y-4">
<h2 className="font-semibold">
{form.id ? "Gericht bearbeiten" : "Neues Gericht"}
</h2>
@@ -177,6 +219,18 @@ export default function DishManager() {
required
/>
</div>
<ChipPicker
label="Allergene"
items={allergens}
selected={form.allergens}
onToggle={toggleAllergen}
/>
<ChipPicker
label="Zusatzstoffe"
items={additives}
selected={form.additives}
onToggle={toggleAdditive}
/>
<div className="flex gap-2">
<button type="submit" className="btn btn-add">
Speichern
@@ -230,40 +284,48 @@ export default function DishManager() {
</div>
)}
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-md bg-white">
{visible.map((dish) => (
<li key={dish.id} className="flex items-center gap-3 px-4 py-2">
<input
type="checkbox"
checked={selected.has(dish.id)}
onChange={() => toggleOne(dish.id)}
aria-label={`${dish.name} auswählen`}
/>
<span className="mr-auto">
<span className="font-semibold">{dish.name}</span>{" "}
<span className="text-gray-500">– {formatPrice(dish.price)}</span>
</span>
<button
type="button"
className="btn btn-primary"
onClick={() =>
setForm({
id: dish.id,
name: dish.name,
price: String(dish.price).replace(".", ","),
})
}
>
Bearbeiten
</button>
<button
type="button"
className="btn btn-del"
onClick={() => remove(dish)}
>
Löschen
</button>
</li>
))}
{visible.map((dish) => {
const codes = dishCodes(dish);
return (
<li key={dish.id} className="flex items-center gap-3 px-4 py-2">
<input
type="checkbox"
checked={selected.has(dish.id)}
onChange={() => toggleOne(dish.id)}
aria-label={`${dish.name} auswählen`}
/>
<span className="mr-auto">
<span className="font-semibold">{dish.name}</span>
{codes && <sup className="dish-codes">{codes}</sup>}{" "}
<span className="text-gray-500">
– {formatPrice(dish.price)}
</span>
</span>
<button
type="button"
className="btn btn-primary"
onClick={() =>
setForm({
id: dish.id,
name: dish.name,
price: String(dish.price).replace(".", ","),
allergens: dish.allergens ?? [],
additives: dish.additives ?? [],
})
}
>
Bearbeiten
</button>
<button
type="button"
className="btn btn-del"
onClick={() => remove(dish)}
>
Löschen
</button>
</li>
);
})}
{dishes.length === 0 && (
<li className="px-4 py-3 text-gray-500">
Noch keine Gerichte angelegt.