Wochenplan: Gerichtsuche statt langer Dropdown-Liste, Beispieldaten zurück

WeekEditor: "Mahlzeit hinzufügen" zeigt jetzt ein Suchfeld mit
gefilterter Trefferliste (wie die Live-Suche bei Gerichten) statt einer
langen Dropdown-Liste zum Durchscrollen. Ausgewähltes Gericht lässt sich
per ×-Button wieder ändern.

Neues Skript db/seed-demo.sql (+ db/seed-demo.mjs, "npm run
db:seed-demo"): spielt den ursprünglichen Beispieldatensatz ein
(4 Essenszeiten, 14 Allergene, 11 Zusatzstoffe, 18 Testgerichte, zwei
Beispielwochen) – nur für lokale Entwicklung/Demos, kein Teil der
automatischen Migrationen, da frische Deployments weiterhin leer
starten sollen.
This commit is contained in:
2026-07-18 11:35:13 +02:00
parent c60f481aec
commit 3d13ba88f4
5 changed files with 237 additions and 26 deletions
+72 -22
View File
@@ -35,6 +35,19 @@ export default function WeekEditor({ monday }: { monday: string }) {
null
);
const [selectedDish, setSelectedDish] = useState("");
const [dishSearch, setDishSearch] = useState("");
function startAdding(iso: string, section: string) {
setAdding({ iso, section });
setSelectedDish("");
setDishSearch("");
}
function cancelAdding() {
setAdding(null);
setSelectedDish("");
setDishSearch("");
}
const load = useCallback(async () => {
setError(null);
@@ -77,8 +90,7 @@ export default function WeekEditor({ monday }: { monday: string }) {
: formatMessage(dict.weekEditor.saveError, { message: data.error ?? String(res.status) })
);
}
setAdding(null);
setSelectedDish("");
cancelAdding();
await load();
}
@@ -90,6 +102,11 @@ export default function WeekEditor({ monday }: { monday: string }) {
}
const [hintBefore, hintAfter] = dict.weekEditor.newDishesHint.split("{link}");
const selectedDishObj = dishes.find((d) => d.id === selectedDish) ?? null;
const query = dishSearch.trim().toLowerCase();
const filteredDishes = query
? dishes.filter((d) => localized(d, locale).toLowerCase().includes(query))
: dishes;
return (
<div>
@@ -168,18 +185,57 @@ export default function WeekEditor({ monday }: { monday: string }) {
})}
{isAdding ? (
<div className="mt-2 space-y-1 text-left">
<select
className="input"
value={selectedDish}
onChange={(e) => setSelectedDish(e.target.value)}
>
<option value="">{dict.weekEditor.selectDish}</option>
{dishes.map((dish) => (
<option key={dish.id} value={dish.id}>
{localized(dish, locale)} ({formatPrice(dish.price)})
</option>
))}
</select>
{selectedDishObj ? (
<div className="flex items-center gap-1 text-sm">
<span className="mr-auto">
{localized(selectedDishObj, locale)} (
{formatPrice(selectedDishObj.price)})
</span>
<button
type="button"
className="text-gray-400 hover:text-[var(--c-primary)]"
onClick={() => {
setSelectedDish("");
setDishSearch("");
}}
aria-label={dict.weekEditor.changeDish}
title={dict.weekEditor.changeDish}
>
×
</button>
</div>
) : (
<>
<input
type="search"
className="input"
autoFocus
placeholder={dict.dishManager.searchPlaceholder}
value={dishSearch}
onChange={(e) => setDishSearch(e.target.value)}
/>
<ul className="max-h-32 overflow-y-auto border border-gray-200 rounded-md bg-white divide-y divide-gray-100">
{filteredDishes.map((dish) => (
<li key={dish.id}>
<button
type="button"
className="w-full text-left px-2 py-1 text-sm hover:bg-[var(--c-row-bg-alt)]"
onClick={() => setSelectedDish(dish.id)}
>
{localized(dish, locale)} ({formatPrice(dish.price)})
</button>
</li>
))}
{filteredDishes.length === 0 && (
<li className="px-2 py-1 text-sm text-gray-500">
{formatMessage(dict.dishManager.noResults, {
query: dishSearch,
})}
</li>
)}
</ul>
</>
)}
<div className="flex gap-1 justify-center">
<button
type="button"
@@ -192,10 +248,7 @@ export default function WeekEditor({ monday }: { monday: string }) {
<button
type="button"
className="btn btn-neutral"
onClick={() => {
setAdding(null);
setSelectedDish("");
}}
onClick={cancelAdding}
>
{dict.common.cancel}
</button>
@@ -205,10 +258,7 @@ export default function WeekEditor({ monday }: { monday: string }) {
<button
type="button"
className="btn btn-add mt-1"
onClick={() => {
setAdding({ iso: d.iso, section: s.key });
setSelectedDish("");
}}
onClick={() => startAdding(d.iso, s.key)}
>
{dict.weekEditor.addMeal}
</button>
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env node
// Spielt db/seed-demo.sql ein: ersetzt Essenszeiten/Gerichte/Allergene/
// Zusatzstoffe/Wochenplan durch den Beispieldatensatz. Nur für lokale
// Entwicklung/Demos gedacht, läuft nicht automatisch beim Start.
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import pg from "pg";
const sql = readFileSync(
join(dirname(fileURLToPath(import.meta.url)), "seed-demo.sql"),
"utf8"
);
async function main() {
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
try {
await client.query(sql);
console.log("db:seed-demo – Beispieldaten eingespielt.");
} finally {
await client.end();
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
+130
View File
@@ -0,0 +1,130 @@
-- =============================================================
-- Beispieldaten für die lokale Entwicklung/Demo (NICHT Teil der
-- automatischen Migrationen – frische Deployments starten bewusst
-- leer, s. db/migrations/0001_init.sql). Manuell anwendbar über
-- "npm run db:seed-demo".
--
-- Ersetzt vorhandene Essenszeiten/Gerichte/Allergene/Zusatzstoffe/
-- Wochenplan-Einträge durch den ursprünglichen Beispieldatensatz.
-- =============================================================
delete from menu_entries;
delete from dish_suggestions;
delete from dishes;
delete from sections;
delete from allergens;
delete from additives;
-- ---------- Essenszeiten ----------
insert into sections (key, label, label_en, label_pl, time_range, sort) values
('brotzeit', 'Brotzeit', 'Breakfast', 'Śniadanie', '09:00 - 10:00', 1),
('mittagessen', 'Mittagessen', 'Lunch', 'Obiad', '11:30 - 13:30', 2),
('mittagessen_veg', 'Mittagessen vegetarisch', 'Lunch (vegetarian)', 'Obiad (wegetariański)', '11:30 - 13:30', 3),
('suppe', 'Suppe', 'Soup', 'Zupa', '11:30 - 13:30', 4);
-- ---------- Allergene (EU-Kennzeichnung, Buchstaben) ----------
insert into allergens (code, name, name_en, name_pl, sort) values
('A', 'Glutenhaltiges Getreide', 'Cereals containing gluten', 'Zboża zawierające gluten', 1),
('B', 'Krebstiere', 'Crustaceans', 'Skorupiaki', 2),
('C', 'Eier', 'Eggs', 'Jaja', 3),
('D', 'Fisch', 'Fish', 'Ryby', 4),
('E', 'Erdnüsse', 'Peanuts', 'Orzeszki ziemne', 5),
('F', 'Sojabohnen', 'Soybeans', 'Soja', 6),
('G', 'Milch (einschließlich Laktose)', 'Milk (including lactose)', 'Mleko (w tym laktoza)', 7),
('H', 'Schalenfrüchte (Nüsse)', 'Tree nuts', 'Orzechy', 8),
('I', 'Sellerie', 'Celery', 'Seler', 9),
('J', 'Senf', 'Mustard', 'Gorczyca', 10),
('K', 'Sesamsamen', 'Sesame seeds', 'Nasiona sezamu', 11),
('L', 'Schwefeldioxid und Sulfite', 'Sulphur dioxide and sulphites', 'Dwutlenek siarki i siarczyny', 12),
('M', 'Lupinen', 'Lupin', 'Łubin', 13),
('N', 'Weichtiere', 'Molluscs', 'Mięczaki', 14);
-- ---------- Zusatzstoffe (Ziffern) ----------
insert into additives (code, name, name_en, name_pl, sort) values
('1', 'mit Farbstoff', 'with colouring', 'z barwnikiem', 1),
('2', 'mit Konservierungsstoff', 'with preservative', 'z konserwantem', 2),
('3', 'mit Antioxidationsmittel', 'with antioxidant', 'z przeciwutleniaczem', 3),
('4', 'mit Geschmacksverstärker', 'with flavour enhancer', 'ze wzmacniaczem smaku', 4),
('5', 'geschwefelt', 'sulphured', 'siarkowany', 5),
('6', 'geschwärzt', 'blackened', 'barwiony na czarno', 6),
('7', 'gewachst', 'waxed', 'woskowany', 7),
('8', 'mit Phosphat', 'with phosphate', 'z fosforanem', 8),
('9', 'mit Süßungsmittel(n)', 'with sweetener(s)', 'ze słodzikiem/słodzikami', 9),
('10', 'enthält eine Phenylalaninquelle', 'contains a source of phenylalanine', 'zawiera źródło fenyloalaniny', 10),
('11', 'koffeinhaltig', 'caffeinated', 'zawiera kofeinę', 11);
-- ---------- Testgerichte ----------
insert into dishes (name, price) values
-- Brotzeit
('1 Stk. Rindskäsewurst mit Brötchen', 2.30),
('Hackfleischbaguette', 3.20),
('Knusperhähnchen Wrap', 3.70),
('Leberkäse im Brötchen', 2.30),
('Chili con Carne mit Brötchen', 3.90),
-- Mittagessen
('Schnitzel Baguette', 3.50),
('Penne mit Grünen Spargel und Garnelenspieße', 5.90),
('Bratkartoffel Gemüse Pfanne mit Hähnchenbrust und Dip', 5.90),
('Schweinefilet im Speckmantel dazu Rahmsoße und Butterspätzle', 5.90),
-- Mittagessen vegetarisch
('Penne mit Grünen Spargel, get. Tomaten und Cherrytomaten', 5.30),
('Spaghetti mit fruchtiger Tomatensauce und Parmesan', 5.30),
('Gebackener Feta mit griechischen Bauernsalat und Baguette', 5.30),
('Gemüse-Curry mit Basmatireis', 4.90),
-- Suppen
('Kartoffelcremesuppe', 1.50),
('Gulaschsuppe', 2.50),
('Tomatensuppe mit Croutons', 1.80),
('Kürbiscremesuppe', 1.80),
('Linseneintopf mit Würstchen', 2.20);
-- ---------- Beispielwoche 13.–17. Juli 2026 ----------
with d as (select id, name from dishes)
insert into menu_entries (menu_date, section_key, dish_id)
select x.menu_date::date, x.section_key, d.id
from (values
('2026-07-13', 'brotzeit', '1 Stk. Rindskäsewurst mit Brötchen'),
('2026-07-14', 'brotzeit', 'Hackfleischbaguette'),
('2026-07-15', 'brotzeit', 'Knusperhähnchen Wrap'),
('2026-07-16', 'brotzeit', 'Leberkäse im Brötchen'),
('2026-07-17', 'brotzeit', 'Chili con Carne mit Brötchen'),
('2026-07-13', 'mittagessen', 'Schnitzel Baguette'),
('2026-07-14', 'mittagessen', 'Penne mit Grünen Spargel und Garnelenspieße'),
('2026-07-15', 'mittagessen', 'Bratkartoffel Gemüse Pfanne mit Hähnchenbrust und Dip'),
('2026-07-16', 'mittagessen', 'Schweinefilet im Speckmantel dazu Rahmsoße und Butterspätzle'),
('2026-07-14', 'mittagessen_veg', 'Penne mit Grünen Spargel, get. Tomaten und Cherrytomaten'),
('2026-07-15', 'mittagessen_veg', 'Spaghetti mit fruchtiger Tomatensauce und Parmesan'),
('2026-07-16', 'mittagessen_veg', 'Gebackener Feta mit griechischen Bauernsalat und Baguette'),
('2026-07-13', 'suppe', 'Kartoffelcremesuppe'),
('2026-07-14', 'suppe', 'Gulaschsuppe'),
('2026-07-15', 'suppe', 'Tomatensuppe mit Croutons'),
('2026-07-16', 'suppe', 'Kürbiscremesuppe'),
('2026-07-17', 'suppe', 'Linseneintopf mit Würstchen')
) as x (menu_date, section_key, name)
join d on d.name = x.name;
-- ---------- Beispielwoche 20.–24. Juli 2026 ("Nächste Woche") ----------
with d as (select id, name from dishes)
insert into menu_entries (menu_date, section_key, dish_id)
select x.menu_date::date, x.section_key, d.id
from (values
('2026-07-20', 'brotzeit', 'Leberkäse im Brötchen'),
('2026-07-21', 'brotzeit', 'Knusperhähnchen Wrap'),
('2026-07-22', 'brotzeit', '1 Stk. Rindskäsewurst mit Brötchen'),
('2026-07-23', 'brotzeit', 'Hackfleischbaguette'),
('2026-07-24', 'brotzeit', 'Chili con Carne mit Brötchen'),
('2026-07-20', 'mittagessen', 'Bratkartoffel Gemüse Pfanne mit Hähnchenbrust und Dip'),
('2026-07-21', 'mittagessen', 'Schweinefilet im Speckmantel dazu Rahmsoße und Butterspätzle'),
('2026-07-22', 'mittagessen', 'Schnitzel Baguette'),
('2026-07-23', 'mittagessen', 'Penne mit Grünen Spargel und Garnelenspieße'),
('2026-07-20', 'mittagessen_veg', 'Gemüse-Curry mit Basmatireis'),
('2026-07-21', 'mittagessen_veg', 'Spaghetti mit fruchtiger Tomatensauce und Parmesan'),
('2026-07-22', 'mittagessen_veg', 'Gebackener Feta mit griechischen Bauernsalat und Baguette'),
('2026-07-23', 'mittagessen_veg', 'Penne mit Grünen Spargel, get. Tomaten und Cherrytomaten'),
('2026-07-20', 'suppe', 'Gulaschsuppe'),
('2026-07-21', 'suppe', 'Kartoffelcremesuppe'),
('2026-07-22', 'suppe', 'Linseneintopf mit Würstchen'),
('2026-07-23', 'suppe', 'Tomatensuppe mit Croutons'),
('2026-07-24', 'suppe', 'Kürbiscremesuppe')
) as x (menu_date, section_key, name)
join d on d.name = x.name;
+3 -3
View File
@@ -68,7 +68,7 @@ const de = {
weekEditor: {
title: "Wochenplan",
loading: "Lade Wochenplan…",
selectDish: "– Gericht wählen –",
changeDish: "Anderes Gericht wählen",
addMeal: "Mahlzeit hinzufügen",
newDishesHint: "Neue Gerichte legst du unter {link} an.",
newDishesLinkLabel: "Gerichte",
@@ -322,7 +322,7 @@ const en: Dict = {
weekEditor: {
title: "Weekly plan",
loading: "Loading weekly plan…",
selectDish: "– Select dish –",
changeDish: "Choose a different dish",
addMeal: "Add meal",
newDishesHint: "Add new dishes under {link}.",
newDishesLinkLabel: "Dishes",
@@ -574,7 +574,7 @@ const pl: Dict = {
weekEditor: {
title: "Plan tygodnia",
loading: "Wczytywanie planu tygodnia…",
selectDish: "– Wybierz danie –",
changeDish: "Wybierz inne danie",
addMeal: "Dodaj posiłek",
newDishesHint: "Nowe dania możesz dodać w zakładce {link}.",
newDishesLinkLabel: "Dania",
+2 -1
View File
@@ -9,7 +9,8 @@
"prestart": "node --env-file-if-exists=.env.local db/migrate.mjs && node --env-file-if-exists=.env.local scripts/bootstrap-admin.mjs",
"start": "next start -H 0.0.0.0",
"lint": "eslint",
"db:migrate": "node --env-file-if-exists=.env.local db/migrate.mjs"
"db:migrate": "node --env-file-if-exists=.env.local db/migrate.mjs",
"db:seed-demo": "node --env-file-if-exists=.env.local db/seed-demo.mjs"
},
"dependencies": {
"@simplewebauthn/browser": "^13.3.0",