From 111966c3aec31ad2be6da45a2fbc9e9bff946aa3 Mon Sep 17 00:00:00 2001 From: kobold-ai Date: Thu, 15 Oct 2026 17:31:29 +0200 Subject: [PATCH] UI & Features: Einkaufsliste im Kochbuch-Design, Supermarkt-Bereiche mit Laufweg-Sortierung, Textumbruch & schlanker Header --- app/database.py | 311 ++++++- app/main.py | 99 ++- app/templates/cookbook.html | 2 +- app/templates/shopping_list.html | 1407 ++++++++++++++++++++++++------ 4 files changed, 1521 insertions(+), 298 deletions(-) diff --git a/app/database.py b/app/database.py index 9d97af4..0c03691 100755 --- a/app/database.py +++ b/app/database.py @@ -1,6 +1,6 @@ import os import sqlite3 -from datetime import datetime +from datetime import datetime, date from typing import List, Dict, Any, Optional try: @@ -60,6 +60,113 @@ CHAPTER_MAP = {c["title"]: c["number"] for c in CHAPTERS} for c in CHAPTERS: CHAPTER_MAP[str(c["number"])] = c["number"] +# Standard-Kategorien für die Einkaufsliste (Supermarkt-Laufweg) +DEFAULT_SHOPPING_CATEGORIES = [ + {"name": "Obst & Gemüse", "icon": "🥦", "sort_order": 1}, + {"name": "Brot & Backwaren", "icon": "🍞", "sort_order": 2}, + {"name": "Kühlregal & Käse", "icon": "🧀", "sort_order": 3}, + {"name": "Fleisch & Fisch", "icon": "🥩", "sort_order": 4}, + {"name": "Vorrat & Nudeln", "icon": "🥫", "sort_order": 5}, + {"name": "Gewürze & Öle", "icon": "🧂", "sort_order": 6}, + {"name": "Tiefkühl", "icon": "❄️", "sort_order": 7}, + {"name": "Getränke", "icon": "🧃", "sort_order": 8}, + {"name": "Drogerie & Sonstiges", "icon": "🧺", "sort_order": 9}, +] + + +def clean_source_label(source: Optional[str]) -> str: + """Extrahiert einen sauberen, kurzen Rezepttitel aus der Quellangabe (ohne Wochenplan-Präfixe).""" + if not source: + return "" + src = str(source).strip() + import re + m = re.search(r'\((.+)\)$', src) + if m: + return m.group(1).strip() + return src + + +def guess_category_for_ingredient(name: str) -> str: + """Bestimmt automatisch die passende Supermarkt-Kategorie anhand deutscher Zutaten-Keywords.""" + if not name: + return "Drogerie & Sonstiges" + n = name.lower() + + # 1. Tiefkühl + if any(k in n for k in ["tk", "tiefkühl", "gefroren", "eiscreme", "speiseeis", "erbsen (tk)", "tk-"]): + return "Tiefkühl" + + # 2. Obst & Gemüse + if any(k in n for k in [ + "zwiebel", "knoblauch", "kartoffel", "karotte", "möhre", "tomate", "paprika", + "brokkoli", "blumenkohl", "spinat", "zucchini", "aubergine", "gurke", "salat", + "lauch", "porree", "frühlingszwiebel", "sellerie", "kohlrabi", "champignon", + "pilz", "steinpilz", "pfifferling", "apfel", "äpfel", "birne", "banane", + "avocado", "zitrone", "limette", "orange", "beere", "erdbeere", "himbeere", + "heidelbeere", "blaubeere", "petersilie", "schnittlauch", "basilikum", "koriander", + "ingwer", "chili", "peperoni", "kürbis", "radieschen", "rucola", "rosmarin", + "minze", "dill", "salbei", "kräuter", "kohl", "wirsing" + ]): + return "Obst & Gemüse" + + # 3. Brot & Backwaren + if any(k in n for k in [ + "brot", "brötchen", "semmel", "semmeln", "toast", "baguette", "fladenbrot", + "tortilla", "wrap", "panko", "paniermehl", "hefe", "backpulver", "vanillezucker", + "blätterteig", "croissant" + ]): + return "Brot & Backwaren" + + # 4. Kühlregal & Käse + if any(k in n for k in [ + "milch", "butter", "sahne", "schmand", "quark", "joghurt", "käse", "cheddar", + "parmesan", "mozzarella", "feta", "gouda", "bergkäse", "frischkäse", + "crème fraîche", "creme fraiche", "mascarpone", "ricotta", "ei", "eier", + "tofu", "sauerrahm", "buttermilch" + ]): + return "Kühlregal & Käse" + + # 5. Fleisch & Fisch + if any(k in n for k in [ + "hackfleisch", "hack", "rinderhack", "hähnchen", "huhn", "pute", "speck", + "schinken", "wurst", "würstchen", "lachs", "garnele", "garnelen", "fisch", + "forelle", "kabeljau", "rind", "schwein", "kalb", "lamm", "steak", "filet", + "bratwurst", "wiener" + ]): + return "Fleisch & Fisch" + + # 6. Gewürze & Öle + if any(k in n for k in [ + "öl", "olivenöl", "rapsöl", "sonnenblumenöl", "sesamöl", "bratöl", "essig", + "balsamico", "sojasauce", "salz", "pfeffer", "paprikapulver", "curry", + "kreuzkümmel", "kümmel", "oregano", "thymian", "majoran", "muskat", + "cayennepfeffer", "zimt", "lorbeer", "vanille", "kurkuma", "chiliflocken" + ]): + return "Gewürze & Öle" + + # 7. Vorrat & Nudeln + if any(k in n for k in [ + "nudel", "nudeln", "spaghetti", "penne", "mie", "pasta", "reis", "basmati", + "jasmin", "linsen", "kichererbsen", "bohnen", "kidneybohnen", "mais", + "tomatenmark", "passierte tomaten", "gehackte tomaten", "dosentomaten", + "mehl", "zucker", "puderzucker", "haferflocken", "kokosmilch", "brühe", + "gemüsebrühe", "rinderbrühe", "hühnerbrühe", "couscous", "bulgur", "quinoa", + "erdnussbutter", "marmelade", "honig", "ahornsirup", "senf", "ketchup", + "mayonnaise", "schokolade", "zartbitterschokolade", "kakao", "nüsse", "mandeln", + "walnüsse", "sesam", "chiasamen", "leinsamen" + ]): + return "Vorrat & Nudeln" + + # 8. Getränke + if any(k in n for k in [ + "wasser", "mineralwasser", "saft", "apfelsaft", "orangensaft", "wein", + "weißwein", "rotwein", "bier", "kaffee", "espresso", "tee", "cola", + "limonade", "sirup" + ]): + return "Getränke" + + return "Drogerie & Sonstiges" + def get_connection(): if DB_ENGINE == "mariadb": @@ -201,12 +308,33 @@ def init_db(): unit VARCHAR(50) DEFAULT '', checked TINYINT(1) NOT NULL DEFAULT 0, source VARCHAR(255) DEFAULT '', + category VARCHAR(100) DEFAULT 'Drogerie & Sonstiges', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_shop_checked (checked) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; """) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS shopping_categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + icon VARCHAR(50) DEFAULT '🛒', + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + """) + + try: + cursor.execute(""" + SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = %s AND TABLE_NAME = 'shopping_list' AND COLUMN_NAME = 'category' + """, (MARIADB_NAME,)) + if not cursor.fetchone(): + cursor.execute("ALTER TABLE shopping_list ADD COLUMN category VARCHAR(100) DEFAULT 'Drogerie & Sonstiges' AFTER source") + except Exception: + pass + cursor.execute("DELETE FROM dishes") else: cursor = conn.cursor() @@ -273,16 +401,35 @@ def init_db(): unit TEXT DEFAULT '', checked INTEGER NOT NULL DEFAULT 0, source TEXT DEFAULT '', + category TEXT DEFAULT 'Drogerie & Sonstiges', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) + cursor.execute(""" + CREATE TABLE IF NOT EXISTS shopping_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE COLLATE NOCASE, + icon TEXT DEFAULT '🛒', + sort_order INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + cursor.execute("PRAGMA table_info(shopping_list)") + existing_shop_cols = [c[1] for c in cursor.fetchall()] + if "category" not in existing_shop_cols: + cursor.execute("ALTER TABLE shopping_list ADD COLUMN category TEXT DEFAULT 'Drogerie & Sonstiges'") + cursor.execute("DELETE FROM dishes") conn.commit() finally: conn.close() + # Standard-Kategorien initialisieren & Altdaten abgleichen + seed_default_shopping_categories_if_empty() + # --- Dishes (Zufallsgenerator) --- @@ -781,29 +928,170 @@ def clear_meal_plan_week(year: int, week: int) -> int: return res.get("rowcount", 0) -# --- Shopping List (Einkaufsliste) --- +# --- Shopping Categories & Shopping List (Einkaufsliste) --- + +def seed_default_shopping_categories_if_empty(): + """Initialisiert die Standard-Kategorien, falls noch keine vorhanden sind, und kategorisiert alte Einträge.""" + try: + cats = execute_query("SELECT COUNT(*) as cnt FROM shopping_categories", fetch="one") + cnt = cats["cnt"] if cats else 0 + if cnt == 0: + for c in DEFAULT_SHOPPING_CATEGORIES: + execute_query( + "INSERT INTO shopping_categories (name, icon, sort_order) VALUES (?, ?, ?)", + (c["name"], c["icon"], c["sort_order"]), + fetch="none", + commit=True + ) + # Bestehende Einträge ohne Kategorie automatisch zuweisen + items = execute_query("SELECT id, item_name, category FROM shopping_list", fetch="all") + for it in items: + cur_cat = it.get("category") + if not cur_cat or cur_cat in ("", "None", "null", "Sonstiges") or cur_cat == "Drogerie & Sonstiges": + guessed = guess_category_for_ingredient(it.get("item_name", "")) + if guessed != cur_cat: + execute_query("UPDATE shopping_list SET category = ? WHERE id = ?", (guessed, it["id"]), fetch="none", commit=True) + except Exception as e: + print(f"Hinweis beim Initialisieren der Shopping-Kategorien: {e}") + + +def get_shopping_categories() -> List[Dict[str, Any]]: + """Gibt alle Supermarkt-Bereiche/Kategorien sortiert nach Reihenfolge zurück.""" + query = "SELECT id, name, icon, sort_order, created_at FROM shopping_categories ORDER BY sort_order ASC, id ASC" + rows = execute_query(query, fetch="all") + if not rows: + seed_default_shopping_categories_if_empty() + rows = execute_query(query, fetch="all") + for r in rows: + if isinstance(r.get("created_at"), (datetime, date)): + r["created_at"] = r["created_at"].isoformat() + return rows + + +def add_shopping_category(name: str, icon: str = "🛒", sort_order: Optional[int] = None) -> Dict[str, Any]: + """Fügt einen neuen Supermarkt-Bereich hinzu.""" + clean_name = name.strip() + if not clean_name: + raise ValueError("Name des Bereichs darf nicht leer sein.") + clean_icon = (icon or "🛒").strip() + + if sort_order is None: + max_row = execute_query("SELECT MAX(sort_order) as m FROM shopping_categories", fetch="one") + sort_order = ((max_row["m"] or 0) + 1) if max_row else 1 + + query = "INSERT INTO shopping_categories (name, icon, sort_order) VALUES (?, ?, ?)" + res = execute_query(query, (clean_name, clean_icon, sort_order), fetch="none", commit=True) + cat_id = res.get("lastrowid") + row = execute_query("SELECT * FROM shopping_categories WHERE id = ?", (cat_id,), fetch="one") + if row and isinstance(row.get("created_at"), (datetime, date)): + row["created_at"] = row["created_at"].isoformat() + return row + + +def update_shopping_category(cat_id: int, name: str, icon: str, sort_order: Optional[int] = None) -> Optional[Dict[str, Any]]: + """Aktualisiert einen Supermarkt-Bereich und benennt vorhandene Zuweisungen um.""" + old = execute_query("SELECT * FROM shopping_categories WHERE id = ?", (cat_id,), fetch="one") + if not old: + return None + + clean_name = name.strip() + clean_icon = (icon or "🛒").strip() + order_val = sort_order if sort_order is not None else old.get("sort_order", 0) + + query = "UPDATE shopping_categories SET name = ?, icon = ?, sort_order = ? WHERE id = ?" + execute_query(query, (clean_name, clean_icon, order_val, cat_id), fetch="none", commit=True) + + # Falls der Name geändert wurde, aktualisiere die Items in shopping_list + if old["name"] != clean_name: + execute_query("UPDATE shopping_list SET category = ? WHERE category = ?", (clean_name, old["name"]), fetch="none", commit=True) + + row = execute_query("SELECT * FROM shopping_categories WHERE id = ?", (cat_id,), fetch="one") + if row and isinstance(row.get("created_at"), (datetime, date)): + row["created_at"] = row["created_at"].isoformat() + return row + + +def delete_shopping_category(cat_id: int) -> bool: + """Löscht einen Bereich. Zugeordnete Items werden auf einen Ausweichbereich gelegt.""" + old = execute_query("SELECT * FROM shopping_categories WHERE id = ?", (cat_id,), fetch="one") + if not old: + return False + + fallback = execute_query("SELECT name FROM shopping_categories WHERE id != ? ORDER BY sort_order DESC LIMIT 1", (cat_id,), fetch="one") + fallback_name = fallback["name"] if fallback else "Drogerie & Sonstiges" + + execute_query("UPDATE shopping_list SET category = ? WHERE category = ?", (fallback_name, old["name"]), fetch="none", commit=True) + res = execute_query("DELETE FROM shopping_categories WHERE id = ?", (cat_id,), fetch="none", commit=True) + return res.get("rowcount", 0) > 0 + + +def reorder_shopping_categories(category_ids: List[int]) -> bool: + """Aktualisiert die Reihenfolge der Supermarkt-Bereiche.""" + for idx, cid in enumerate(category_ids): + execute_query("UPDATE shopping_categories SET sort_order = ? WHERE id = ?", (idx + 1, cid), fetch="none", commit=True) + return True + + +def reset_default_shopping_categories() -> List[Dict[str, Any]]: + """Setzt alle Bereiche auf die Standard-Reihenfolge zurück.""" + execute_query("DELETE FROM shopping_categories", fetch="none", commit=True) + seed_default_shopping_categories_if_empty() + return get_shopping_categories() + + +def update_shopping_item_category(item_id: int, category: str) -> Optional[Dict[str, Any]]: + """Ändert die Kategorie/den Bereich eines einzelnen Listeneintrags.""" + clean_cat = category.strip() + query = "UPDATE shopping_list SET category = ? WHERE id = ?" + execute_query(query, (clean_cat, item_id), fetch="none", commit=True) + row = execute_query("SELECT * FROM shopping_list WHERE id = ?", (item_id,), fetch="one") + if row: + row["checked"] = bool(row.get("checked")) + row["display_source"] = clean_source_label(row.get("source", "")) + if isinstance(row.get("created_at"), (datetime, date)): + row["created_at"] = row["created_at"].isoformat() + if isinstance(row.get("updated_at"), (datetime, date)): + row["updated_at"] = row["updated_at"].isoformat() + return row + def get_shopping_list() -> List[Dict[str, Any]]: """Gibt alle Einträge der Einkaufsliste zurück, sortiert nach unvollständig/erledigt.""" query = """ - SELECT id, item_name, amount, unit, checked, source, created_at, updated_at + SELECT id, item_name, amount, unit, checked, source, category, created_at, updated_at FROM shopping_list ORDER BY checked ASC, id DESC """ rows = execute_query(query, fetch="all") for r in rows: r["checked"] = bool(r.get("checked")) + if not r.get("category"): + r["category"] = guess_category_for_ingredient(r.get("item_name", "")) + r["display_source"] = clean_source_label(r.get("source", "")) + if isinstance(r.get("created_at"), (datetime, date)): + r["created_at"] = r["created_at"].isoformat() + if isinstance(r.get("updated_at"), (datetime, date)): + r["updated_at"] = r["updated_at"].isoformat() return rows -def add_shopping_item(item_name: str, amount: str = "", unit: str = "", source: str = "") -> Dict[str, Any]: +def add_shopping_item(item_name: str, amount: str = "", unit: str = "", source: str = "", category: Optional[str] = None) -> Dict[str, Any]: """Fügt einen einzelnen Eintrag zur Einkaufsliste hinzu.""" - query = "INSERT INTO shopping_list (item_name, amount, unit, source, checked) VALUES (?, ?, ?, ?, 0)" - res = execute_query(query, (item_name.strip(), amount.strip(), unit.strip(), source.strip()), fetch="none", commit=True) + cat = (category or "").strip() + if not cat: + cat = guess_category_for_ingredient(item_name) + clean_src = clean_source_label(source) + query = "INSERT INTO shopping_list (item_name, amount, unit, source, category, checked) VALUES (?, ?, ?, ?, ?, 0)" + res = execute_query(query, (item_name.strip(), amount.strip(), unit.strip(), clean_src, cat), fetch="none", commit=True) item_id = res.get("lastrowid") row = execute_query("SELECT * FROM shopping_list WHERE id = ?", (item_id,), fetch="one") if row: row["checked"] = bool(row.get("checked")) + row["display_source"] = clean_source_label(row.get("source", "")) + if isinstance(row.get("created_at"), (datetime, date)): + row["created_at"] = row["created_at"].isoformat() + if isinstance(row.get("updated_at"), (datetime, date)): + row["updated_at"] = row["updated_at"].isoformat() return row @@ -818,9 +1106,13 @@ def add_shopping_items_bulk(items: List[Dict[str, Any]]) -> int: continue amt = str(it.get("amount") or "").strip() unit = str(it.get("unit") or "").strip() - src = str(it.get("source") or it.get("recipe_title") or "").strip() - query = "INSERT INTO shopping_list (item_name, amount, unit, source, checked) VALUES (?, ?, ?, ?, 0)" - execute_query(query, (name, amt, unit, src), fetch="none", commit=True) + raw_src = str(it.get("source") or it.get("recipe_title") or "").strip() + src = clean_source_label(raw_src) + cat = str(it.get("category") or "").strip() + if not cat: + cat = guess_category_for_ingredient(name) + query = "INSERT INTO shopping_list (item_name, amount, unit, source, category, checked) VALUES (?, ?, ?, ?, ?, 0)" + execute_query(query, (name, amt, unit, src, cat), fetch="none", commit=True) count += 1 return count @@ -832,6 +1124,7 @@ def toggle_shopping_item(item_id: int) -> Optional[Dict[str, Any]]: row = execute_query("SELECT * FROM shopping_list WHERE id = ?", (item_id,), fetch="one") if row: row["checked"] = bool(row.get("checked")) + row["display_source"] = clean_source_label(row.get("source", "")) return row diff --git a/app/main.py b/app/main.py index 89e20ee..67c1397 100755 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,5 @@ import os +import json import uuid import random import shutil @@ -49,7 +50,14 @@ from .database import ( delete_shopping_item, delete_checked_shopping_items, clear_shopping_list, - get_meal_plan_ingredients + get_meal_plan_ingredients, + get_shopping_categories, + add_shopping_category, + update_shopping_category, + delete_shopping_category, + reorder_shopping_categories, + reset_default_shopping_categories, + update_shopping_item_category ) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -185,10 +193,27 @@ class ShoppingItemCreate(BaseModel): amount: Optional[str] = "" unit: Optional[str] = "" source: Optional[str] = "" + category: Optional[str] = None class ShoppingBulkCreate(BaseModel): items: List[ShoppingItemCreate] +class ShoppingCategoryCreate(BaseModel): + name: str + icon: Optional[str] = "🛒" + sort_order: Optional[int] = None + +class ShoppingCategoryUpdate(BaseModel): + name: str + icon: Optional[str] = "🛒" + sort_order: Optional[int] = None + +class ShoppingReorderRequest(BaseModel): + category_ids: List[int] + +class ShoppingItemCategoryUpdate(BaseModel): + category: str + # ========================================== # AUTH VIEWS @@ -281,13 +306,26 @@ async def home_view(request: Request): async def shopping_list_view(request: Request): """Einkaufsliste Übersicht""" items = get_shopping_list() + categories = get_shopping_categories() today = date.today() iso_year, iso_week, _ = today.isocalendar() + + def json_serial(obj): + if isinstance(obj, (datetime, date)): + return obj.isoformat() + return str(obj) + + items_json = json.dumps(items, default=json_serial) + categories_json = json.dumps(categories, default=json_serial) + return templates.TemplateResponse( request=request, name="shopping_list.html", context={ "items": items, + "categories": categories, + "items_json": items_json, + "categories_json": categories_json, "current_year": iso_year, "current_week": iso_week } @@ -914,7 +952,8 @@ def api_clear_mealplan_week(year: int = Query(...), week: int = Query(...)): @app.get("/api/shopping-list") def api_get_shopping_list(): items = get_shopping_list() - return {"status": "ok", "items": items} + categories = get_shopping_categories() + return {"status": "ok", "items": items, "categories": categories} @app.post("/api/shopping-list") @@ -925,7 +964,8 @@ def api_add_shopping_item(payload: ShoppingItemCreate): item_name=payload.item_name, amount=payload.amount or "", unit=payload.unit or "", - source=payload.source or "" + source=payload.source or "", + category=payload.category ) return {"status": "ok", "item": item} @@ -945,6 +985,14 @@ def api_toggle_shopping_item(item_id: int): return {"status": "ok", "item": updated} +@app.post("/api/shopping-list/{item_id}/category") +def api_update_item_category(item_id: int, payload: ShoppingItemCategoryUpdate): + updated = update_shopping_item_category(item_id, payload.category) + if not updated: + raise HTTPException(status_code=404, detail="Eintrag nicht gefunden.") + return {"status": "ok", "item": updated} + + @app.delete("/api/shopping-list/checked") def api_delete_checked_shopping(): count = delete_checked_shopping_items() @@ -963,6 +1011,51 @@ def api_delete_shopping_item(item_id: int): return {"status": "ok", "deleted": success} +# ========================================== +# SHOPPING CATEGORIES API (SUPERMARKT-BEREICHE) +# ========================================== + +@app.get("/api/shopping-categories") +def api_get_shopping_categories(): + categories = get_shopping_categories() + return {"status": "ok", "categories": categories} + + +@app.post("/api/shopping-categories") +def api_add_shopping_category(payload: ShoppingCategoryCreate): + try: + cat = add_shopping_category(payload.name, payload.icon or "🛒", payload.sort_order) + return {"status": "ok", "category": cat} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@app.put("/api/shopping-categories/{cat_id}") +def api_update_shopping_category(cat_id: int, payload: ShoppingCategoryUpdate): + cat = update_shopping_category(cat_id, payload.name, payload.icon or "🛒", payload.sort_order) + if not cat: + raise HTTPException(status_code=404, detail="Bereich nicht gefunden.") + return {"status": "ok", "category": cat} + + +@app.delete("/api/shopping-categories/{cat_id}") +def api_delete_shopping_category(cat_id: int): + success = delete_shopping_category(cat_id) + return {"status": "ok", "deleted": success} + + +@app.post("/api/shopping-categories/reorder") +def api_reorder_shopping_categories(payload: ShoppingReorderRequest): + reorder_shopping_categories(payload.category_ids) + return {"status": "ok", "categories": get_shopping_categories()} + + +@app.post("/api/shopping-categories/reset-defaults") +def api_reset_default_shopping_categories(): + cats = reset_default_shopping_categories() + return {"status": "ok", "categories": cats} + + # ========================================== # MEAL PLAN INGREDIENTS API # ========================================== diff --git a/app/templates/cookbook.html b/app/templates/cookbook.html index 83ae8be..8bdc684 100755 --- a/app/templates/cookbook.html +++ b/app/templates/cookbook.html @@ -2677,7 +2677,7 @@ item_name: cb.getAttribute('data-name'), amount: cb.getAttribute('data-amount') || '', unit: cb.getAttribute('data-unit') || '', - source: `Wochenplan KW ${mpCurrentWeek} (${cb.getAttribute('data-recipe')})` + source: cb.getAttribute('data-recipe') || 'Wochenplan' })); try { diff --git a/app/templates/shopping_list.html b/app/templates/shopping_list.html index 5fa3d0d..3e55d2d 100644 --- a/app/templates/shopping_list.html +++ b/app/templates/shopping_list.html @@ -2,279 +2,403 @@ - + Einkaufsliste – What To Cook? - + - +
- + ⬅️ Zurück
- 🛒 -

Einkaufsliste

+

🛒 Einkaufsliste

- ✕ + +
- +
+

Einkaufszettel

-

Woche {{ current_week }} • Zutaten & Besorgungen

+

Nach Supermarkt-Gängen sortiert • Woche {{ current_week }}

0 / {{ items|length }} erledigt
- +
- - +
+ + +
+
+ Bereich: + +
- +
- + 📅 Aus Wochenplan übernehmen
- -
- -
    - {% for item in items %} -
  • - - -
  • - {% endfor %} -
+ +
+ +
- -
+ + + + + @@ -535,9 +989,58 @@
+ + + + + +
✓ Gespeichert
+ + +