UI & Features: Einkaufsliste im Kochbuch-Design, Supermarkt-Bereiche mit Laufweg-Sortierung, Textumbruch & schlanker Header
This commit is contained in:
+302
-9
@@ -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
|
||||
|
||||
|
||||
|
||||
+96
-3
@@ -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
|
||||
# ==========================================
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+1122
-285
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user