From 868a5d2fcfe9e0dd52591dc29c0e49caad11af48 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 18 Jul 2026 08:14:32 +0200 Subject: [PATCH 1/7] =?UTF-8?q?Bereiche=20vollst=C3=A4ndig=20verwaltbar=20?= =?UTF-8?q?(anlegen/bearbeiten/l=C3=B6schen/Uhrzeit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration 0006: sections.key bekommt einen automatisch generierten Schlüssel (wie dishes.id), menu_entries.section_key kaskadiert beim Löschen eines Bereichs, neue RLS-Policy erlaubt Kantinenmitarbeitern/ Admins Schreibzugriff auf sections. Neue Seite „Bereiche" im Kantinen-Bereich (SectionManager): Bereiche anlegen, umbenennen (inkl. EN/PL), Uhrzeit setzen, löschen und per Pfeil-Buttons neu anordnen. --- app/intern/bereiche/page.tsx | 5 + components/AppHeader.tsx | 1 + components/SectionManager.tsx | 298 ++++++++++++++++++ lib/i18n/dictionaries.ts | 57 ++++ .../migrations/0006_variable_sections.sql | 25 ++ 5 files changed, 386 insertions(+) create mode 100644 app/intern/bereiche/page.tsx create mode 100644 components/SectionManager.tsx create mode 100644 supabase/migrations/0006_variable_sections.sql diff --git a/app/intern/bereiche/page.tsx b/app/intern/bereiche/page.tsx new file mode 100644 index 0000000..57a346d --- /dev/null +++ b/app/intern/bereiche/page.tsx @@ -0,0 +1,5 @@ +import SectionManager from "@/components/SectionManager"; + +export default function BereichePage() { + return ; +} diff --git a/components/AppHeader.tsx b/components/AppHeader.tsx index 4a651d1..2c89e6f 100644 --- a/components/AppHeader.tsx +++ b/components/AppHeader.tsx @@ -23,6 +23,7 @@ export default function AppHeader({ const menuLinks = [ { href: "/intern", label: dict.header.weekPlan }, + { href: "/intern/bereiche", label: dict.header.sections }, { href: "/intern/gerichte", label: dict.header.dishes }, { href: "/intern/kennzeichnung", label: dict.header.labels }, { href: "/intern/vorschlaege", label: dict.header.suggestions }, diff --git a/components/SectionManager.tsx b/components/SectionManager.tsx new file mode 100644 index 0000000..f537be0 --- /dev/null +++ b/components/SectionManager.tsx @@ -0,0 +1,298 @@ +"use client"; + +// Verwaltung der Speiseplan-Bereiche (Brotzeit, Mittagessen, …): +// anlegen, umbenennen, Uhrzeit ändern, löschen, Reihenfolge anpassen. + +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 { Section } from "@/lib/types"; +import { useDict, useLocale } from "@/components/i18n/LocaleProvider"; + +interface SectionForm { + key: string | null; // null = neuer Bereich + label: string; + labelEn: string; + labelPl: string; + timeFrom: string; + timeTo: string; +} + +const EMPTY_FORM: SectionForm = { + key: null, + label: "", + labelEn: "", + labelPl: "", + timeFrom: "", + timeTo: "", +}; + +function splitTimeRange(range: string): { from: string; to: string } { + const [from, to] = range.split("-").map((s) => 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} +

+ +
+ + {error && ( +

+ {error} +

+ )} + + {form && ( +
+

+ {form.key ? dict.sectionManager.editSection : dict.sectionManager.newSection} +

+
+ + setForm({ ...form, label: e.target.value })} + required + /> +
+
+
+ + setForm({ ...form, labelEn: e.target.value })} + /> +
+
+ + setForm({ ...form, labelPl: e.target.value })} + /> +
+
+
+
+ + setForm({ ...form, timeFrom: e.target.value })} + required + /> +
+
+ + setForm({ ...form, timeTo: e.target.value })} + required + /> +
+
+
+ + +
+
+ )} + + {loading ? ( +

{dict.common.loading}

+ ) : ( +
    + {sections.map((section, idx) => ( +
  • +
    + + +
    + + {localized(section, locale)}{" "} + – {section.time_range} + + + +
  • + ))} + {sections.length === 0 && ( +
  • {dict.sectionManager.empty}
  • + )} +
+ )} +
+ ); +} diff --git a/lib/i18n/dictionaries.ts b/lib/i18n/dictionaries.ts index de5254e..71c32f8 100644 --- a/lib/i18n/dictionaries.ts +++ b/lib/i18n/dictionaries.ts @@ -30,6 +30,7 @@ const de = { canteen: "Kantine", menuGroup: "Speiseplan", weekPlan: "Wochenplan", + sections: "Bereiche", dishes: "Gerichte", labels: "Allergene & Zusatzstoffe", adminGroup: "Verwaltung", @@ -120,6 +121,24 @@ const de = { saveError: "Speichern fehlgeschlagen: {message}", deleteError: "Löschen fehlgeschlagen: {message}", }, + sectionManager: { + title: "Bereiche", + newSection: "Neuer Bereich", + editSection: "Bereich bearbeiten", + nameLabel: "Bezeichnung", + nameEnLabel: "Bezeichnung (Englisch)", + namePlLabel: "Bezeichnung (Polnisch)", + timeFrom: "Von", + timeTo: "Bis", + moveUp: "Nach oben", + moveDown: "Nach unten", + empty: "Noch keine Bereiche 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 +288,7 @@ const en: Dict = { canteen: "Canteen", menuGroup: "Menu", weekPlan: "Weekly plan", + sections: "Sections", dishes: "Dishes", labels: "Allergens & Additives", adminGroup: "Administration", @@ -359,6 +379,24 @@ const en: Dict = { saveError: "Saving failed: {message}", deleteError: "Deleting failed: {message}", }, + sectionManager: { + title: "Sections", + newSection: "New section", + editSection: "Edit section", + nameLabel: "Name", + nameEnLabel: "Name (English)", + namePlLabel: "Name (Polish)", + timeFrom: "From", + timeTo: "To", + moveUp: "Move up", + moveDown: "Move down", + empty: "No sections 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 +544,7 @@ const pl: Dict = { canteen: "Stołówka", menuGroup: "Jadłospis", weekPlan: "Plan tygodnia", + sections: "Sekcje", dishes: "Dania", labels: "Alergeny i dodatki", adminGroup: "Administracja", @@ -596,6 +635,24 @@ const pl: Dict = { saveError: "Zapisywanie nie powiodło się: {message}", deleteError: "Usuwanie nie powiodło się: {message}", }, + sectionManager: { + title: "Sekcje", + newSection: "Nowa sekcja", + editSection: "Edytuj sekcję", + 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 sekcji.", + 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/supabase/migrations/0006_variable_sections.sql b/supabase/migrations/0006_variable_sections.sql new file mode 100644 index 0000000..629a50f --- /dev/null +++ b/supabase/migrations/0006_variable_sections.sql @@ -0,0 +1,25 @@ +-- ============================================================= +-- Bereiche (Brotzeit/Mittagessen/…) vollständig verwaltbar machen: +-- anlegen, umbenennen, Uhrzeit ändern, löschen. +-- ============================================================= + +-- Neue Bereiche 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 einen Bereich, werden seine 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 Bereiche 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')); From 51fdb93e97a3d1505a55cb975e465af760633015 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 18 Jul 2026 08:14:49 +0200 Subject: [PATCH 2/7] =?UTF-8?q?Automatisch-=C3=BCbersetzen-Button=20nur=20?= =?UTF-8?q?bei=20konfiguriertem=20DEEPL=5FAPI=5FKEY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/intern/gerichte/page.tsx | 2 +- app/intern/kennzeichnung/page.tsx | 5 +-- components/CodeListManager.tsx | 53 +++++++++++++--------------- components/DishManager.tsx | 57 +++++++++++++++---------------- lib/i18n/dictionaries.ts | 6 ---- 5 files changed, 55 insertions(+), 68 deletions(-) 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/components/CodeListManager.tsx b/components/CodeListManager.tsx index ed3b429..1a677dd 100644 --- a/components/CodeListManager.tsx +++ b/components/CodeListManager.tsx @@ -12,8 +12,10 @@ 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(); @@ -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 === "done" && ( - - {dict.dishManager.translated} - - )} - {translateState === "unavailable" && ( - - {dict.dishManager.translateUnavailable} - - )} -
+ {translateEnabled && ( +
+ + {translateState === "done" && ( + + {dict.dishManager.translated} + + )} +
+ )}
-
- - {translateState === "done" && ( - - {dict.dishManager.translated} - - )} - {translateState === "unavailable" && ( - - {dict.dishManager.translateUnavailable} - - )} -
+ {translateEnabled && ( +
+ + {translateState === "done" && ( + + {dict.dishManager.translated} + + )} +
+ )} Date: Sat, 18 Jul 2026 08:15:20 +0200 Subject: [PATCH 3/7] Lokale PostgreSQL-Kopie: Dump nach Migration 0006 aktualisiert --- supabase/local/10_schema.sql | 14 +++++++++----- supabase/local/20_data.sql | 12 ++++++------ 2 files changed, 15 insertions(+), 11 deletions(-) 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 From d85d2e3fe38ce3b21e50217dbbc8c98d1f93a924 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 18 Jul 2026 08:15:31 +0200 Subject: [PATCH 4/7] Version 1.4.0 --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c9f6b..c87b380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ 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 (Branch `feature/v1.4`, noch nicht gemergt) + +### Hinzugefügt +- Bereiche (Brotzeit/Mittagessen/…) vollständig verwaltbar: anlegen, + umbenennen (inkl. EN/PL), Uhrzeit ändern, löschen und per Pfeil-Buttons + neu anordnen – neue Seite „Bereiche" 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. + ## [1.3.0] – 2026-07-18 ### Hinzugefügt 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", From 0f20ad6527b7f1ecce9878c67a618b1665c9e9e2 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 18 Jul 2026 08:26:15 +0200 Subject: [PATCH 5/7] "Bereiche" zu "Essenszeiten" umbenannt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Betrifft UI-Texte (DE/EN/PL), Route (/intern/bereiche -> /intern/essenszeiten), Code-Kommentare, Changelog und README. Fachlich unverändert (weiterhin die Tabelle/Komponente "sections"/ SectionManager im Code), da "Essenszeiten" den Zeitbezug des Features klarer ausdrückt als der bisherige Sammelbegriff. --- CHANGELOG.md | 4 +-- README.md | 8 ++--- .../{bereiche => essenszeiten}/page.tsx | 2 +- components/AppHeader.tsx | 2 +- components/SectionManager.tsx | 4 +-- lib/i18n/dictionaries.ts | 30 +++++++++---------- lib/menu.ts | 2 +- supabase/migrations/0001_init.sql | 4 +-- .../migrations/0006_variable_sections.sql | 8 ++--- 9 files changed, 32 insertions(+), 32 deletions(-) rename app/intern/{bereiche => essenszeiten}/page.tsx (66%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c87b380..7e0543e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ Versionierung nach [Semantic Versioning](https://semver.org/lang/de/). ## [1.4.0] – 2026-07-18 (Branch `feature/v1.4`, noch nicht gemergt) ### Hinzugefügt -- Bereiche (Brotzeit/Mittagessen/…) vollständig verwaltbar: anlegen, +- Essenszeiten (Brotzeit/Mittagessen/…) vollständig verwaltbar: anlegen, umbenennen (inkl. EN/PL), Uhrzeit ändern, löschen und per Pfeil-Buttons - neu anordnen – neue Seite „Bereiche" im Kantinen-Bereich. Löschen + neu anordnen – neue Seite „Essenszeiten" im Kantinen-Bereich. Löschen entfernt zugewiesene Gerichte automatisch aus allen Wochenplänen. ### Geändert 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/bereiche/page.tsx b/app/intern/essenszeiten/page.tsx similarity index 66% rename from app/intern/bereiche/page.tsx rename to app/intern/essenszeiten/page.tsx index 57a346d..c92b122 100644 --- a/app/intern/bereiche/page.tsx +++ b/app/intern/essenszeiten/page.tsx @@ -1,5 +1,5 @@ import SectionManager from "@/components/SectionManager"; -export default function BereichePage() { +export default function EssenszeitenPage() { return ; } diff --git a/components/AppHeader.tsx b/components/AppHeader.tsx index 2c89e6f..23b42c5 100644 --- a/components/AppHeader.tsx +++ b/components/AppHeader.tsx @@ -23,7 +23,7 @@ export default function AppHeader({ const menuLinks = [ { href: "/intern", label: dict.header.weekPlan }, - { href: "/intern/bereiche", label: dict.header.sections }, + { href: "/intern/essenszeiten", label: dict.header.sections }, { href: "/intern/gerichte", label: dict.header.dishes }, { href: "/intern/kennzeichnung", label: dict.header.labels }, { href: "/intern/vorschlaege", label: dict.header.suggestions }, diff --git a/components/SectionManager.tsx b/components/SectionManager.tsx index f537be0..2c714e5 100644 --- a/components/SectionManager.tsx +++ b/components/SectionManager.tsx @@ -1,6 +1,6 @@ "use client"; -// Verwaltung der Speiseplan-Bereiche (Brotzeit, Mittagessen, …): +// Verwaltung der Essenszeiten (Brotzeit, Mittagessen, …): // anlegen, umbenennen, Uhrzeit ändern, löschen, Reihenfolge anpassen. import { useCallback, useEffect, useMemo, useState } from "react"; @@ -11,7 +11,7 @@ import type { Section } from "@/lib/types"; import { useDict, useLocale } from "@/components/i18n/LocaleProvider"; interface SectionForm { - key: string | null; // null = neuer Bereich + key: string | null; // null = neue Essenszeit label: string; labelEn: string; labelPl: string; diff --git a/lib/i18n/dictionaries.ts b/lib/i18n/dictionaries.ts index c3082ee..27f5792 100644 --- a/lib/i18n/dictionaries.ts +++ b/lib/i18n/dictionaries.ts @@ -30,7 +30,7 @@ const de = { canteen: "Kantine", menuGroup: "Speiseplan", weekPlan: "Wochenplan", - sections: "Bereiche", + sections: "Essenszeiten", dishes: "Gerichte", labels: "Allergene & Zusatzstoffe", adminGroup: "Verwaltung", @@ -120,9 +120,9 @@ const de = { deleteError: "Löschen fehlgeschlagen: {message}", }, sectionManager: { - title: "Bereiche", - newSection: "Neuer Bereich", - editSection: "Bereich bearbeiten", + title: "Essenszeiten", + newSection: "Neue Essenszeit", + editSection: "Essenszeit bearbeiten", nameLabel: "Bezeichnung", nameEnLabel: "Bezeichnung (Englisch)", namePlLabel: "Bezeichnung (Polnisch)", @@ -130,7 +130,7 @@ const de = { timeTo: "Bis", moveUp: "Nach oben", moveDown: "Nach unten", - empty: "Noch keine Bereiche angelegt.", + empty: "Noch keine Essenszeiten angelegt.", confirmDelete: "„{name}“ wirklich löschen? Zugewiesene Gerichte werden auch aus allen Wochenplänen entfernt.", loadError: "Laden fehlgeschlagen: {message}", @@ -286,7 +286,7 @@ const en: Dict = { canteen: "Canteen", menuGroup: "Menu", weekPlan: "Weekly plan", - sections: "Sections", + sections: "Meal times", dishes: "Dishes", labels: "Allergens & Additives", adminGroup: "Administration", @@ -376,9 +376,9 @@ const en: Dict = { deleteError: "Deleting failed: {message}", }, sectionManager: { - title: "Sections", - newSection: "New section", - editSection: "Edit section", + title: "Meal times", + newSection: "New meal time", + editSection: "Edit meal time", nameLabel: "Name", nameEnLabel: "Name (English)", namePlLabel: "Name (Polish)", @@ -386,7 +386,7 @@ const en: Dict = { timeTo: "To", moveUp: "Move up", moveDown: "Move down", - empty: "No sections yet.", + empty: "No meal times yet.", confirmDelete: "Really delete “{name}”? Assigned dishes will also be removed from all weekly plans.", loadError: "Loading failed: {message}", @@ -540,7 +540,7 @@ const pl: Dict = { canteen: "Stołówka", menuGroup: "Jadłospis", weekPlan: "Plan tygodnia", - sections: "Sekcje", + sections: "Pory posiłków", dishes: "Dania", labels: "Alergeny i dodatki", adminGroup: "Administracja", @@ -630,9 +630,9 @@ const pl: Dict = { deleteError: "Usuwanie nie powiodło się: {message}", }, sectionManager: { - title: "Sekcje", - newSection: "Nowa sekcja", - editSection: "Edytuj sekcję", + title: "Pory posiłków", + newSection: "Nowa pora posiłku", + editSection: "Edytuj porę posiłku", nameLabel: "Nazwa", nameEnLabel: "Nazwa (angielski)", namePlLabel: "Nazwa (polski)", @@ -640,7 +640,7 @@ const pl: Dict = { timeTo: "Do", moveUp: "Przesuń w górę", moveDown: "Przesuń w dół", - empty: "Nie dodano jeszcze żadnych sekcji.", + 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}", 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/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 index 629a50f..b636681 100644 --- a/supabase/migrations/0006_variable_sections.sql +++ b/supabase/migrations/0006_variable_sections.sql @@ -1,14 +1,14 @@ -- ============================================================= --- Bereiche (Brotzeit/Mittagessen/…) vollständig verwaltbar machen: +-- Essenszeiten (Brotzeit/Mittagessen/…) vollständig verwaltbar machen: -- anlegen, umbenennen, Uhrzeit ändern, löschen. -- ============================================================= --- Neue Bereiche bekommen einen automatisch generierten Schlüssel +-- 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 einen Bereich, werden seine Wochenplan-Einträge mit +-- 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 @@ -18,7 +18,7 @@ alter table public.menu_entries 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 Bereiche auch anlegen/ändern/löschen. +-- 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')) From 4d5e7fca04c99d131ae266f9593142643c66120c Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Sat, 18 Jul 2026 08:30:20 +0200 Subject: [PATCH 6/7] Bug: Sprachumschalter in mobiler Ansicht doppelt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Kopfbereich der öffentlichen Speisekarte zeigte seinen eigenen LanguageSwitcher auch auf schmalen Bildschirmen an, obwohl PublicMobileMenu dort bereits einen eigenen rendert. Der Header-Switcher ist jetzt wie der Rest des Desktop-Layouts erst ab dem sm-Breakpoint sichtbar. --- CHANGELOG.md | 4 ++++ app/page.tsx | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e0543e..cffebc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ Versionierung nach [Semantic Versioning](https://semver.org/lang/de/). 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/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. */} +
+ +
Date: Sat, 18 Jul 2026 08:33:17 +0200 Subject: [PATCH 7/7] =?UTF-8?q?Changelog:=20Hinweis=20auf=20noch=20offenen?= =?UTF-8?q?=20Merge=20f=C3=BCr=201.4.0=20entfernt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cffebc2..005ebb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 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 (Branch `feature/v1.4`, noch nicht gemergt) +## [1.4.0] – 2026-07-18 ### Hinzugefügt - Essenszeiten (Brotzeit/Mittagessen/…) vollständig verwaltbar: anlegen,