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?
-
+
-
+
-
+
+
-
+
-
+
-
-
-
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+ Passe die Bereiche an deinen gewohnten Supermarkt an. Mit den Pfeilen ▲ und ▼ bestimmst du die Reihenfolge deiner Gänge beim Einkaufen.
+
+
+
+
+
+
+
+
+ Neuen Bereich hinzufügen
+
+
+
+
+
+
+ ↺ Standard-Bereiche wiederherstellen
+
+
+
+
+
+
+
✓ Gespeichert
+
+
+