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:
+68
-18
@@ -35,6 +35,19 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [selectedDish, setSelectedDish] = useState("");
|
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 () => {
|
const load = useCallback(async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -77,8 +90,7 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
: formatMessage(dict.weekEditor.saveError, { message: data.error ?? String(res.status) })
|
: formatMessage(dict.weekEditor.saveError, { message: data.error ?? String(res.status) })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setAdding(null);
|
cancelAdding();
|
||||||
setSelectedDish("");
|
|
||||||
await load();
|
await load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +102,11 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [hintBefore, hintAfter] = dict.weekEditor.newDishesHint.split("{link}");
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -168,18 +185,57 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
})}
|
})}
|
||||||
{isAdding ? (
|
{isAdding ? (
|
||||||
<div className="mt-2 space-y-1 text-left">
|
<div className="mt-2 space-y-1 text-left">
|
||||||
<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"
|
className="input"
|
||||||
value={selectedDish}
|
autoFocus
|
||||||
onChange={(e) => setSelectedDish(e.target.value)}
|
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)}
|
||||||
>
|
>
|
||||||
<option value="">{dict.weekEditor.selectDish}</option>
|
|
||||||
{dishes.map((dish) => (
|
|
||||||
<option key={dish.id} value={dish.id}>
|
|
||||||
{localized(dish, locale)} ({formatPrice(dish.price)})
|
{localized(dish, locale)} ({formatPrice(dish.price)})
|
||||||
</option>
|
</button>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</select>
|
{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">
|
<div className="flex gap-1 justify-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -192,10 +248,7 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-neutral"
|
className="btn btn-neutral"
|
||||||
onClick={() => {
|
onClick={cancelAdding}
|
||||||
setAdding(null);
|
|
||||||
setSelectedDish("");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{dict.common.cancel}
|
{dict.common.cancel}
|
||||||
</button>
|
</button>
|
||||||
@@ -205,10 +258,7 @@ export default function WeekEditor({ monday }: { monday: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-add mt-1"
|
className="btn btn-add mt-1"
|
||||||
onClick={() => {
|
onClick={() => startAdding(d.iso, s.key)}
|
||||||
setAdding({ iso: d.iso, section: s.key });
|
|
||||||
setSelectedDish("");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{dict.weekEditor.addMeal}
|
{dict.weekEditor.addMeal}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
@@ -68,7 +68,7 @@ const de = {
|
|||||||
weekEditor: {
|
weekEditor: {
|
||||||
title: "Wochenplan",
|
title: "Wochenplan",
|
||||||
loading: "Lade Wochenplan…",
|
loading: "Lade Wochenplan…",
|
||||||
selectDish: "– Gericht wählen –",
|
changeDish: "Anderes Gericht wählen",
|
||||||
addMeal: "Mahlzeit hinzufügen",
|
addMeal: "Mahlzeit hinzufügen",
|
||||||
newDishesHint: "Neue Gerichte legst du unter {link} an.",
|
newDishesHint: "Neue Gerichte legst du unter {link} an.",
|
||||||
newDishesLinkLabel: "Gerichte",
|
newDishesLinkLabel: "Gerichte",
|
||||||
@@ -322,7 +322,7 @@ const en: Dict = {
|
|||||||
weekEditor: {
|
weekEditor: {
|
||||||
title: "Weekly plan",
|
title: "Weekly plan",
|
||||||
loading: "Loading weekly plan…",
|
loading: "Loading weekly plan…",
|
||||||
selectDish: "– Select dish –",
|
changeDish: "Choose a different dish",
|
||||||
addMeal: "Add meal",
|
addMeal: "Add meal",
|
||||||
newDishesHint: "Add new dishes under {link}.",
|
newDishesHint: "Add new dishes under {link}.",
|
||||||
newDishesLinkLabel: "Dishes",
|
newDishesLinkLabel: "Dishes",
|
||||||
@@ -574,7 +574,7 @@ const pl: Dict = {
|
|||||||
weekEditor: {
|
weekEditor: {
|
||||||
title: "Plan tygodnia",
|
title: "Plan tygodnia",
|
||||||
loading: "Wczytywanie planu tygodnia…",
|
loading: "Wczytywanie planu tygodnia…",
|
||||||
selectDish: "– Wybierz danie –",
|
changeDish: "Wybierz inne danie",
|
||||||
addMeal: "Dodaj posiłek",
|
addMeal: "Dodaj posiłek",
|
||||||
newDishesHint: "Nowe dania możesz dodać w zakładce {link}.",
|
newDishesHint: "Nowe dania możesz dodać w zakładce {link}.",
|
||||||
newDishesLinkLabel: "Dania",
|
newDishesLinkLabel: "Dania",
|
||||||
|
|||||||
+2
-1
@@ -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",
|
"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",
|
"start": "next start -H 0.0.0.0",
|
||||||
"lint": "eslint",
|
"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": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.3.0",
|
"@simplewebauthn/browser": "^13.3.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user