diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37c9f6b..005ebb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,23 @@ Alle nennenswerten Änderungen an diesem Projekt werden hier dokumentiert.
Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
Versionierung nach [Semantic Versioning](https://semver.org/lang/de/).
+## [1.4.0] – 2026-07-18
+
+### Hinzugefügt
+- Essenszeiten (Brotzeit/Mittagessen/…) vollständig verwaltbar: anlegen,
+ umbenennen (inkl. EN/PL), Uhrzeit ändern, löschen und per Pfeil-Buttons
+ neu anordnen – neue Seite „Essenszeiten" im Kantinen-Bereich. Löschen
+ entfernt zugewiesene Gerichte automatisch aus allen Wochenplänen.
+
+### Geändert
+- „Automatisch übersetzen"-Button bei Gerichten/Allergenen/Zusatzstoffen
+ ist nur noch sichtbar, wenn `DEEPL_API_KEY` gesetzt ist, statt erst
+ nach Klick auf die fehlende Konfiguration hinzuweisen.
+
+### Behoben
+- Sprachumschalter (DE/EN/PL) erschien auf der öffentlichen Speisekarte
+ in der mobilen Ansicht doppelt.
+
## [1.3.0] – 2026-07-18
### Hinzugefügt
diff --git a/README.md b/README.md
index c34d483..ccfca73 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,8 @@ markieren die jeweiligen Versionsstände (`git tag`).
| Bereich | Funktionen |
| --- | --- |
-| Öffentlich (ohne Login) | Wochenplan Mo–Fr mit den Bereichen Brotzeit (09:00–10:00), Mittagessen, Mittagessen vegetarisch und Suppe (je 11:30–13:30); Navigation vorherige/nächste Woche/Heute; auf schmalen Bildschirmen umschaltbare Tages-/Wochenansicht (Tag ist Standard); Logo zentriert über der Karte; Allergen-/Zusatzstoff-Kürzel je Gericht, Legende unterhalb der Karte; Sprachumschalter Deutsch/Englisch/Polnisch; Rezeptvorschläge einreichen |
-| Kantinen-Team (`/intern`) | Gerichte anlegen/bearbeiten/löschen (inkl. Live-Suche, Mehrfachauswahl zum Batch-Löschen, englischem/polnischem Namen mit automatischer DeepL-Übersetzung) mit Allergenen/Zusatzstoffen je Gericht; Gerichte Tagen/Bereichen zuweisen (mehrere pro Zelle) und wieder entfernen; globale Allergen-/Zusatzstofflisten pflegen (ebenfalls mehrsprachig); eingereichte Rezeptvorschläge sichten, übernehmen oder ablehnen; eigenes Konto verwalten (Name, E-Mail, Passwort, Passkeys, Passwort-Anmeldung an/aus) |
+| Öffentlich (ohne Login) | Wochenplan Mo–Fr mit den Essenszeiten Brotzeit (09:00–10:00), Mittagessen, Mittagessen vegetarisch und Suppe (je 11:30–13:30); Navigation vorherige/nächste Woche/Heute; auf schmalen Bildschirmen umschaltbare Tages-/Wochenansicht (Tag ist Standard); Logo zentriert über der Karte; Allergen-/Zusatzstoff-Kürzel je Gericht, Legende unterhalb der Karte; Sprachumschalter Deutsch/Englisch/Polnisch; Rezeptvorschläge einreichen |
+| Kantinen-Team (`/intern`) | Gerichte anlegen/bearbeiten/löschen (inkl. Live-Suche, Mehrfachauswahl zum Batch-Löschen, englischem/polnischem Namen mit automatischer DeepL-Übersetzung) mit Allergenen/Zusatzstoffen je Gericht; Gerichte Tagen/Essenszeiten zuweisen (mehrere pro Zelle) und wieder entfernen; globale Allergen-/Zusatzstofflisten pflegen (ebenfalls mehrsprachig); eingereichte Rezeptvorschläge sichten, übernehmen oder ablehnen; eigenes Konto verwalten (Name, E-Mail, Passwort, Passkeys, Passwort-Anmeldung an/aus) |
| Admin (`/admin`) | Kantinenmitarbeiter anlegen/bearbeiten/löschen (inkl. Passwort-Reset mit Wiederholung und Rollenvergabe); Farbgebung der Speisekarte anpassen; Logo und Favicon hochladen/entfernen |
Anmeldung mit E-Mail + Passwort oder alternativ per **Passkey**
@@ -24,7 +24,7 @@ Passwort-Anmeldung kann pro Konto deaktiviert werden, sobald mindestens
ein Passkey registriert ist.
Die gesamte Oberfläche ist auf Deutsch, Englisch und Polnisch verfügbar
-(Sprachumschalter, Cookie-basiert). Gerichte, Bereiche, Allergene und
+(Sprachumschalter, Cookie-basiert). Gerichte, Essenszeiten, Allergene und
Zusatzstoffe lassen sich zusätzlich auf Englisch/Polnisch benennen –
mit optionaler automatischer Übersetzung über DeepL (siehe
`DEEPL_API_KEY` unten) und jederzeit manuell korrigierbar.
@@ -136,7 +136,7 @@ ihre Gültigkeit und müssen neu registriert werden.
Sicherheitsmodell (Row Level Security):
-- `anon` darf ausschließlich lesen (Speisekarte, Bereiche, Allergene,
+- `anon` darf ausschließlich lesen (Speisekarte, Essenszeiten, Allergene,
Zusatzstoffe, Einstellungen).
- `staff`/`admin` (Rolle im `profiles`-Eintrag) pflegen Gerichte,
Wochenplan und Kennzeichnungen.
diff --git a/app/intern/essenszeiten/page.tsx b/app/intern/essenszeiten/page.tsx
new file mode 100644
index 0000000..c92b122
--- /dev/null
+++ b/app/intern/essenszeiten/page.tsx
@@ -0,0 +1,5 @@
+import SectionManager from "@/components/SectionManager";
+
+export default function EssenszeitenPage() {
+ return ;
+}
diff --git a/app/intern/gerichte/page.tsx b/app/intern/gerichte/page.tsx
index 96de7e9..c6ef73b 100644
--- a/app/intern/gerichte/page.tsx
+++ b/app/intern/gerichte/page.tsx
@@ -1,5 +1,5 @@
import DishManager from "@/components/DishManager";
export default function GerichtePage() {
- return ;
+ return ;
}
diff --git a/app/intern/kennzeichnung/page.tsx b/app/intern/kennzeichnung/page.tsx
index d965d2a..dcfbaed 100644
--- a/app/intern/kennzeichnung/page.tsx
+++ b/app/intern/kennzeichnung/page.tsx
@@ -1,10 +1,11 @@
import CodeListManager from "@/components/CodeListManager";
export default function KennzeichnungPage() {
+ const translateEnabled = !!process.env.DEEPL_API_KEY;
return (
-
-
+
+
);
}
diff --git a/app/page.tsx b/app/page.tsx
index c6eb51a..b326795 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -63,7 +63,11 @@ export default async function Home({
kollidieren. */}
-
+ {/* Auf Mobilgeräten übernimmt PublicMobileMenu den
+ Sprachumschalter, hier würde er sonst doppelt erscheinen. */}
+
+
+
createClient(), []);
const dict = useDict();
@@ -34,9 +36,9 @@ export default function CodeListManager({
namePl: string;
} | null>(null);
const [error, setError] = useState
(null);
- const [translateState, setTranslateState] = useState<
- "idle" | "busy" | "done" | "unavailable"
- >("idle");
+ const [translateState, setTranslateState] = useState<"idle" | "busy" | "done">(
+ "idle"
+ );
const load = useCallback(async () => {
const { data, error } = await supabase.from(table).select("*").order("sort");
@@ -115,10 +117,6 @@ export default function CodeListManager({
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error ?? `Fehler (${res.status})`);
- if (data.configured === false) {
- setTranslateState("unavailable");
- return;
- }
setEditing((prev) =>
prev ? { ...prev, nameEn: data.en ?? prev.nameEn, namePl: data.pl ?? prev.namePl } : prev
);
@@ -191,28 +189,25 @@ export default function CodeListManager({
/>
-
-
- {translateState === "busy"
- ? dict.dishManager.translating
- : dict.dishManager.autoTranslate}
-
- {translateState === "done" && (
-
- {dict.dishManager.translated}
-
- )}
- {translateState === "unavailable" && (
-
- {dict.dishManager.translateUnavailable}
-
- )}
-
+ {translateEnabled && (
+
+
+ {translateState === "busy"
+ ? dict.dishManager.translating
+ : dict.dishManager.autoTranslate}
+
+ {translateState === "done" && (
+
+ {dict.dishManager.translated}
+
+ )}
+
+ )}
{dict.common.save}
diff --git a/components/DishManager.tsx b/components/DishManager.tsx
index edf3c28..cd69b7f 100644
--- a/components/DishManager.tsx
+++ b/components/DishManager.tsx
@@ -29,7 +29,11 @@ const EMPTY_FORM: DishForm = {
additives: [],
};
-export default function DishManager() {
+export default function DishManager({
+ translateEnabled,
+}: {
+ translateEnabled: boolean;
+}) {
const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
@@ -42,9 +46,9 @@ export default function DishManager() {
const [selected, setSelected] = useState>(new Set());
const [bulkBusy, setBulkBusy] = useState(false);
const [search, setSearch] = useState("");
- const [translateState, setTranslateState] = useState<
- "idle" | "busy" | "done" | "unavailable"
- >("idle");
+ const [translateState, setTranslateState] = useState<"idle" | "busy" | "done">(
+ "idle"
+ );
// Live-Suche: filtert die Liste bereits während des Tippens
const visible = useMemo(() => {
@@ -199,10 +203,6 @@ export default function DishManager() {
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error ?? `Fehler (${res.status})`);
- if (data.configured === false) {
- setTranslateState("unavailable");
- return;
- }
setForm((prev) =>
prev ? { ...prev, nameEn: data.en ?? prev.nameEn, namePl: data.pl ?? prev.namePl } : prev
);
@@ -296,28 +296,25 @@ export default function DishManager() {
/>
-
-
- {translateState === "busy"
- ? dict.dishManager.translating
- : dict.dishManager.autoTranslate}
-
- {translateState === "done" && (
-
- {dict.dishManager.translated}
-
- )}
- {translateState === "unavailable" && (
-
- {dict.dishManager.translateUnavailable}
-
- )}
-
+ {translateEnabled && (
+
+
+ {translateState === "busy"
+ ? dict.dishManager.translating
+ : dict.dishManager.autoTranslate}
+
+ {translateState === "done" && (
+
+ {dict.dishManager.translated}
+
+ )}
+
+ )}
s.trim());
+ return { from: from ?? "", to: to ?? "" };
+}
+
+export default function SectionManager() {
+ const supabase = useMemo(() => createClient(), []);
+ const dict = useDict();
+ const locale = useLocale();
+ const [sections, setSections] = useState([]);
+ const [form, setForm] = useState(null);
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [busy, setBusy] = useState(false);
+
+ const load = useCallback(async () => {
+ const { data, error } = await supabase.from("sections").select("*").order("sort");
+ if (error)
+ setError(formatMessage(dict.sectionManager.loadError, { message: error.message }));
+ else setSections(data as Section[]);
+ setLoading(false);
+ }, [supabase, dict]);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ async function save(e: React.FormEvent) {
+ e.preventDefault();
+ if (!form) return;
+ setError(null);
+ const label = form.label.trim();
+ if (!label || !form.timeFrom || !form.timeTo) return;
+ const payload = {
+ label,
+ label_en: form.labelEn.trim() || null,
+ label_pl: form.labelPl.trim() || null,
+ time_range: `${form.timeFrom} - ${form.timeTo}`,
+ };
+
+ let dbError;
+ if (form.key) {
+ ({ error: dbError } = await supabase
+ .from("sections")
+ .update(payload)
+ .eq("key", form.key));
+ } else {
+ // Frisch aus der DB lesen statt den lokalen State zu nutzen, damit
+ // ein veralteter Snapshot nicht zu doppelten sort-Werten führt.
+ const { data: maxRow } = await supabase
+ .from("sections")
+ .select("sort")
+ .order("sort", { ascending: false })
+ .limit(1)
+ .maybeSingle();
+ ({ error: dbError } = await supabase
+ .from("sections")
+ .insert({ ...payload, sort: (maxRow?.sort ?? 0) + 1 }));
+ }
+ if (dbError) {
+ setError(formatMessage(dict.sectionManager.saveError, { message: dbError.message }));
+ return;
+ }
+ setForm(null);
+ await load();
+ }
+
+ async function remove(section: Section) {
+ if (
+ !window.confirm(
+ formatMessage(dict.sectionManager.confirmDelete, { name: section.label })
+ )
+ )
+ return;
+ const { error } = await supabase.from("sections").delete().eq("key", section.key);
+ if (error)
+ setError(formatMessage(dict.sectionManager.deleteError, { message: error.message }));
+ await load();
+ }
+
+ async function move(section: Section, direction: -1 | 1) {
+ const idx = sections.findIndex((s) => s.key === section.key);
+ const neighbor = sections[idx + direction];
+ if (!neighbor) return;
+ setBusy(true);
+ setError(null);
+ const [r1, r2] = await Promise.all([
+ supabase.from("sections").update({ sort: neighbor.sort }).eq("key", section.key),
+ supabase.from("sections").update({ sort: section.sort }).eq("key", neighbor.key),
+ ]);
+ const dbError = r1.error ?? r2.error;
+ if (dbError)
+ setError(formatMessage(dict.sectionManager.saveError, { message: dbError.message }));
+ setBusy(false);
+ await load();
+ }
+
+ return (
+
+
+
+ {dict.sectionManager.title}
+
+ setForm(EMPTY_FORM)}
+ >
+ {dict.sectionManager.newSection}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {form && (
+
+ )}
+
+ {loading ? (
+
{dict.common.loading}
+ ) : (
+
+ {sections.map((section, idx) => (
+
+
+ move(section, -1)}
+ disabled={busy || idx === 0}
+ aria-label={dict.sectionManager.moveUp}
+ title={dict.sectionManager.moveUp}
+ >
+ ▲
+
+ move(section, 1)}
+ disabled={busy || idx === sections.length - 1}
+ aria-label={dict.sectionManager.moveDown}
+ title={dict.sectionManager.moveDown}
+ >
+ ▼
+
+
+
+ {localized(section, locale)} {" "}
+ – {section.time_range}
+
+ {
+ const { from, to } = splitTimeRange(section.time_range);
+ setForm({
+ key: section.key,
+ label: section.label,
+ labelEn: section.label_en ?? "",
+ labelPl: section.label_pl ?? "",
+ timeFrom: from,
+ timeTo: to,
+ });
+ }}
+ >
+ {dict.common.edit}
+
+ remove(section)}
+ >
+ {dict.common.delete}
+
+
+ ))}
+ {sections.length === 0 && (
+ {dict.sectionManager.empty}
+ )}
+
+ )}
+
+ );
+}
diff --git a/lib/i18n/dictionaries.ts b/lib/i18n/dictionaries.ts
index de5254e..27f5792 100644
--- a/lib/i18n/dictionaries.ts
+++ b/lib/i18n/dictionaries.ts
@@ -30,6 +30,7 @@ const de = {
canteen: "Kantine",
menuGroup: "Speiseplan",
weekPlan: "Wochenplan",
+ sections: "Essenszeiten",
dishes: "Gerichte",
labels: "Allergene & Zusatzstoffe",
adminGroup: "Verwaltung",
@@ -87,8 +88,6 @@ const de = {
autoTranslate: "Automatisch übersetzen",
translating: "Übersetze…",
translated: "Übersetzt.",
- translateUnavailable:
- "Automatische Übersetzung ist nicht eingerichtet. Bitte Englisch/Polnisch manuell ausfüllen.",
priceLabel: "Preis in €",
pricePlaceholder: "3,50",
searchPlaceholder: "Gericht suchen…",
@@ -120,6 +119,24 @@ const de = {
saveError: "Speichern fehlgeschlagen: {message}",
deleteError: "Löschen fehlgeschlagen: {message}",
},
+ sectionManager: {
+ title: "Essenszeiten",
+ newSection: "Neue Essenszeit",
+ editSection: "Essenszeit bearbeiten",
+ nameLabel: "Bezeichnung",
+ nameEnLabel: "Bezeichnung (Englisch)",
+ namePlLabel: "Bezeichnung (Polnisch)",
+ timeFrom: "Von",
+ timeTo: "Bis",
+ moveUp: "Nach oben",
+ moveDown: "Nach unten",
+ empty: "Noch keine Essenszeiten angelegt.",
+ confirmDelete:
+ "„{name}“ wirklich löschen? Zugewiesene Gerichte werden auch aus allen Wochenplänen entfernt.",
+ loadError: "Laden fehlgeschlagen: {message}",
+ saveError: "Speichern fehlgeschlagen: {message}",
+ deleteError: "Löschen fehlgeschlagen: {message}",
+ },
userManager: {
title: "Benutzer",
newUser: "Neuer Benutzer",
@@ -269,6 +286,7 @@ const en: Dict = {
canteen: "Canteen",
menuGroup: "Menu",
weekPlan: "Weekly plan",
+ sections: "Meal times",
dishes: "Dishes",
labels: "Allergens & Additives",
adminGroup: "Administration",
@@ -326,8 +344,6 @@ const en: Dict = {
autoTranslate: "Auto-translate",
translating: "Translating…",
translated: "Translated.",
- translateUnavailable:
- "Automatic translation is not set up. Please fill in English/Polish manually.",
priceLabel: "Price in €",
pricePlaceholder: "3.50",
searchPlaceholder: "Search dishes…",
@@ -359,6 +375,24 @@ const en: Dict = {
saveError: "Saving failed: {message}",
deleteError: "Deleting failed: {message}",
},
+ sectionManager: {
+ title: "Meal times",
+ newSection: "New meal time",
+ editSection: "Edit meal time",
+ nameLabel: "Name",
+ nameEnLabel: "Name (English)",
+ namePlLabel: "Name (Polish)",
+ timeFrom: "From",
+ timeTo: "To",
+ moveUp: "Move up",
+ moveDown: "Move down",
+ empty: "No meal times yet.",
+ confirmDelete:
+ "Really delete “{name}”? Assigned dishes will also be removed from all weekly plans.",
+ loadError: "Loading failed: {message}",
+ saveError: "Saving failed: {message}",
+ deleteError: "Deleting failed: {message}",
+ },
userManager: {
title: "Users",
newUser: "New user",
@@ -506,6 +540,7 @@ const pl: Dict = {
canteen: "Stołówka",
menuGroup: "Jadłospis",
weekPlan: "Plan tygodnia",
+ sections: "Pory posiłków",
dishes: "Dania",
labels: "Alergeny i dodatki",
adminGroup: "Administracja",
@@ -563,8 +598,6 @@ const pl: Dict = {
autoTranslate: "Przetłumacz automatycznie",
translating: "Tłumaczenie…",
translated: "Przetłumaczono.",
- translateUnavailable:
- "Automatyczne tłumaczenie nie jest skonfigurowane. Wypełnij angielski/polski ręcznie.",
priceLabel: "Cena w €",
pricePlaceholder: "3,50",
searchPlaceholder: "Szukaj dania…",
@@ -596,6 +629,24 @@ const pl: Dict = {
saveError: "Zapisywanie nie powiodło się: {message}",
deleteError: "Usuwanie nie powiodło się: {message}",
},
+ sectionManager: {
+ title: "Pory posiłków",
+ newSection: "Nowa pora posiłku",
+ editSection: "Edytuj porę posiłku",
+ nameLabel: "Nazwa",
+ nameEnLabel: "Nazwa (angielski)",
+ namePlLabel: "Nazwa (polski)",
+ timeFrom: "Od",
+ timeTo: "Do",
+ moveUp: "Przesuń w górę",
+ moveDown: "Przesuń w dół",
+ empty: "Nie dodano jeszcze żadnych pór posiłków.",
+ confirmDelete:
+ "Czy na pewno usunąć „{name}”? Przypisane dania zostaną również usunięte ze wszystkich planów tygodniowych.",
+ loadError: "Wczytywanie nie powiodło się: {message}",
+ saveError: "Zapisywanie nie powiodło się: {message}",
+ deleteError: "Usuwanie nie powiodło się: {message}",
+ },
userManager: {
title: "Użytkownicy",
newUser: "Nowy użytkownik",
diff --git a/lib/menu.ts b/lib/menu.ts
index 9cc49fb..5049ec0 100644
--- a/lib/menu.ts
+++ b/lib/menu.ts
@@ -64,7 +64,7 @@ export function dishCodes(dish: { allergens: string[]; additives: string[] }): s
return [...(dish.allergens ?? []), ...(dish.additives ?? [])].join(",");
}
-/** Einträge eines Tages/Bereichs aus der geladenen Wochenliste herausfiltern. */
+/** Einträge eines Tages/einer Essenszeit aus der geladenen Wochenliste herausfiltern. */
export function entriesFor(
entries: MenuEntry[],
iso: string,
diff --git a/package-lock.json b/package-lock.json
index e83b059..be4a23a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "canteen-menu",
- "version": "1.3.0",
+ "version": "1.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "canteen-menu",
- "version": "1.3.0",
+ "version": "1.4.0",
"dependencies": {
"@supabase/ssr": "^0.12.3",
"@supabase/supabase-js": "^2.110.7",
diff --git a/package.json b/package.json
index cfdf261..4f01dae 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "canteen-menu",
- "version": "1.3.0",
+ "version": "1.4.0",
"private": true,
"scripts": {
"dev": "next dev -H 0.0.0.0",
diff --git a/supabase/local/10_schema.sql b/supabase/local/10_schema.sql
index cb7d922..d4d0c90 100644
--- a/supabase/local/10_schema.sql
+++ b/supabase/local/10_schema.sql
@@ -22,9 +22,6 @@ SET row_security = off;
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--
--- Das "public"-Schema existiert im offiziellen postgres-Image bereits;
--- CREATE SCHEMA würde dort mit ON_ERROR_STOP=1 das komplette Init-Skript
--- abbrechen.
CREATE SCHEMA IF NOT EXISTS public;
@@ -191,7 +188,7 @@ CREATE TABLE public.profiles (
--
CREATE TABLE public.sections (
- key text NOT NULL,
+ key text DEFAULT (gen_random_uuid())::text NOT NULL,
label text NOT NULL,
time_range text NOT NULL,
sort integer NOT NULL,
@@ -320,7 +317,7 @@ ALTER TABLE ONLY public.menu_entries
--
ALTER TABLE ONLY public.menu_entries
- ADD CONSTRAINT menu_entries_section_key_fkey FOREIGN KEY (section_key) REFERENCES public.sections(key);
+ ADD CONSTRAINT menu_entries_section_key_fkey FOREIGN KEY (section_key) REFERENCES public.sections(key) ON DELETE CASCADE;
--
@@ -471,6 +468,13 @@ ALTER TABLE public.sections ENABLE ROW LEVEL SECURITY;
CREATE POLICY sections_public_read ON public.sections FOR SELECT TO authenticated, anon USING (true);
+--
+-- Name: sections sections_staff_write; Type: POLICY; Schema: public; Owner: -
+--
+
+CREATE POLICY sections_staff_write ON public.sections TO authenticated USING ((public.app_role() = ANY (ARRAY['staff'::text, 'admin'::text]))) WITH CHECK ((public.app_role() = ANY (ARRAY['staff'::text, 'admin'::text])));
+
+
--
-- Name: settings; Type: ROW SECURITY; Schema: public; Owner: -
--
diff --git a/supabase/local/20_data.sql b/supabase/local/20_data.sql
index 5a38c99..c8732b7 100644
--- a/supabase/local/20_data.sql
+++ b/supabase/local/20_data.sql
@@ -2,7 +2,7 @@
-- PostgreSQL database dump
--
-\restrict m2Vnc4kNXrHeKWS4dSX5SqMU5nUOxMfipXDVyLuYabYYzEbAHutmaRS8m10Dsdh
+\restrict y6aLgjBTAX9fiLwtkHEGaN6baJ1dCr2Yjdb6nmrL5LPA6jgf5Ml1Mm0EpXO56Kk
-- Dumped from database version 17.6
-- Dumped by pg_dump version 17.10
@@ -120,10 +120,10 @@ ALTER TABLE public.dishes ENABLE TRIGGER ALL;
ALTER TABLE public.sections DISABLE TRIGGER ALL;
COPY public.sections (key, label, time_range, sort, label_en, label_pl) FROM stdin;
-brotzeit Brotzeit 09:00 - 10:00 1 Breakfast Śniadanie
-mittagessen Mittagessen 11:30 - 13:30 2 Lunch Obiad
-mittagessen_veg Mittagessen vegetarisch 11:30 - 13:30 3 Lunch (vegetarian) Obiad (wegetariański)
-suppe Suppe 11:30 - 13:30 4 Soup Zupa
+mittagessen_veg Mittagessen vegetarisch 11:30 - 13:30 4 Lunch (vegetarian) Obiad (wegetariański)
+mittagessen Mittagessen 11:30 - 13:30 3 Lunch Obiad
+brotzeit Brotzeit 09:00 - 10:00 2 Breakfast Śniadanie
+suppe Suppe 11:30 - 13:30 6 Soup Zupa
\.
@@ -208,5 +208,5 @@ ALTER TABLE public.settings ENABLE TRIGGER ALL;
-- PostgreSQL database dump complete
--
-\unrestrict m2Vnc4kNXrHeKWS4dSX5SqMU5nUOxMfipXDVyLuYabYYzEbAHutmaRS8m10Dsdh
+\unrestrict y6aLgjBTAX9fiLwtkHEGaN6baJ1dCr2Yjdb6nmrL5LPA6jgf5Ml1Mm0EpXO56Kk
diff --git a/supabase/migrations/0001_init.sql b/supabase/migrations/0001_init.sql
index 3201bd3..fea35d6 100644
--- a/supabase/migrations/0001_init.sql
+++ b/supabase/migrations/0001_init.sql
@@ -49,7 +49,7 @@ create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();
--- ---------- Bereiche der Speisekarte ----------
+-- ---------- Essenszeiten der Speisekarte ----------
create table public.sections (
key text primary key,
label text not null,
@@ -101,7 +101,7 @@ create trigger dishes_touch_updated_at
before update on public.dishes
for each row execute function public.touch_updated_at();
--- ---------- Zuweisung Gericht -> Tag/Bereich ----------
+-- ---------- Zuweisung Gericht -> Tag/Essenszeit ----------
create table public.menu_entries (
id uuid primary key default gen_random_uuid(),
menu_date date not null,
diff --git a/supabase/migrations/0006_variable_sections.sql b/supabase/migrations/0006_variable_sections.sql
new file mode 100644
index 0000000..b636681
--- /dev/null
+++ b/supabase/migrations/0006_variable_sections.sql
@@ -0,0 +1,25 @@
+-- =============================================================
+-- Essenszeiten (Brotzeit/Mittagessen/…) vollständig verwaltbar machen:
+-- anlegen, umbenennen, Uhrzeit ändern, löschen.
+-- =============================================================
+
+-- Neue Essenszeiten bekommen einen automatisch generierten Schlüssel
+-- (wie bei dishes.id) statt eines von Hand vergebenen Slugs.
+alter table public.sections
+ alter column key set default (gen_random_uuid())::text;
+
+-- Löscht man eine Essenszeit, werden ihre Wochenplan-Einträge mit
+-- entfernt (analog zu menu_entries_dish_id_fkey bei Gerichten) statt
+-- die Löschung zu blockieren.
+alter table public.menu_entries
+ drop constraint menu_entries_section_key_fkey;
+alter table public.menu_entries
+ add constraint menu_entries_section_key_fkey
+ foreign key (section_key) references public.sections(key) on delete cascade;
+
+-- Bisher gab es nur eine Lesepolicy für sections; jetzt dürfen
+-- Kantinenmitarbeiter/Admins Essenszeiten auch anlegen/ändern/löschen.
+create policy "sections_staff_write" on public.sections
+ for all to authenticated
+ using (public.app_role() in ('staff', 'admin'))
+ with check (public.app_role() in ('staff', 'admin'));