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
|
||||
);
|
||||
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
|
||||
{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"
|
||||
value={selectedDish}
|
||||
onChange={(e) => setSelectedDish(e.target.value)}
|
||||
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)}
|
||||
>
|
||||
<option value="">{dict.weekEditor.selectDish}</option>
|
||||
{dishes.map((dish) => (
|
||||
<option key={dish.id} value={dish.id}>
|
||||
{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">
|
||||
<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>
|
||||
|
||||
@@ -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: {
|
||||
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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user