Feature: Einstiegsseite, Einkaufsliste, Wochenplan-Live-Suche & Zufallsgenerator-Optimierung

This commit is contained in:
2026-10-15 13:05:08 +02:00
parent 2b8c4a35cf
commit 0d6d0deb69
6 changed files with 2240 additions and 67 deletions
+173 -1
View File
@@ -193,6 +193,20 @@ def init_db():
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS shopping_list (
id INT AUTO_INCREMENT PRIMARY KEY,
item_name VARCHAR(255) NOT NULL,
amount VARCHAR(50) DEFAULT '',
unit VARCHAR(50) DEFAULT '',
checked TINYINT(1) NOT NULL DEFAULT 0,
source VARCHAR(255) DEFAULT '',
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("DELETE FROM dishes")
else:
cursor = conn.cursor()
@@ -251,6 +265,19 @@ def init_db():
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS shopping_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_name TEXT NOT NULL,
amount TEXT DEFAULT '',
unit TEXT DEFAULT '',
checked INTEGER NOT NULL DEFAULT 0,
source TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("DELETE FROM dishes")
conn.commit()
finally:
@@ -461,7 +488,9 @@ def query_recipe_candidates(chapter_number: Optional[int] = None, efforts: Optio
if chapter_number is not None and chapter_number > 0:
pool = [r for r in all_recipes if r.get("chapter_number") == chapter_number]
else:
pool = list(all_recipes)
# Standard beim Zufallsgenerator: Kategorien Desserts (9), Kuchen & Gebäck (10) und Plätzchen (11) ausschließen
EXCLUDED_SHUFFLE_CHAPTERS = {9, 10, 11}
pool = [r for r in all_recipes if r.get("chapter_number") not in EXCLUDED_SHUFFLE_CHAPTERS]
if not pool:
return []
@@ -752,3 +781,146 @@ def clear_meal_plan_week(year: int, week: int) -> int:
return res.get("rowcount", 0)
# --- Shopping List (Einkaufsliste) ---
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
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"))
return rows
def add_shopping_item(item_name: str, amount: str = "", unit: str = "", source: str = "") -> 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)
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"))
return row
def add_shopping_items_bulk(items: List[Dict[str, Any]]) -> int:
"""Fügt mehrere Zutaten/Einträge auf einmal zur Einkaufsliste hinzu."""
if not items:
return 0
count = 0
for it in items:
name = (it.get("item_name") or it.get("name") or it.get("raw") or "").strip()
if not name:
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)
count += 1
return count
def toggle_shopping_item(item_id: int) -> Optional[Dict[str, Any]]:
"""Schaltet den Erledigt-Status eines Eintrags um."""
query = "UPDATE shopping_list SET checked = CASE WHEN checked = 1 THEN 0 ELSE 1 END WHERE id = ?"
execute_query(query, (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"))
return row
def delete_shopping_item(item_id: int) -> bool:
"""Löscht einen einzelnen Eintrag aus der Einkaufsliste."""
query = "DELETE FROM shopping_list WHERE id = ?"
res = execute_query(query, (item_id,), fetch="none", commit=True)
return res.get("rowcount", 0) > 0
def delete_checked_shopping_items() -> int:
"""Löscht alle bereits abgehakten Einträge aus der Einkaufsliste."""
query = "DELETE FROM shopping_list WHERE checked = 1"
res = execute_query(query, fetch="none", commit=True)
return res.get("rowcount", 0)
def clear_shopping_list() -> int:
"""Leert die gesamte Einkaufsliste."""
query = "DELETE FROM shopping_list"
res = execute_query(query, fetch="none", commit=True)
return res.get("rowcount", 0)
def get_meal_plan_ingredients(year: int, week: int) -> List[Dict[str, Any]]:
"""Gibt alle Zutaten aller Rezepte des Wochenplans strukturiert für die Einkaufsliste zurück."""
slots = get_meal_plan(year, week)
if not slots:
return []
from .ingredients import parse_ingredients_block
all_recipes = {r["id"]: r for r in get_all_recipes()}
day_names = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
meal_labels = {"frueh": "Früh", "mittag": "Mittag", "kaffee": "Kaffee & Kuchen", "abend": "Abend"}
recipe_groups = []
seen_slots = set()
for s in slots:
r_id = s.get("recipe_id")
if not r_id or r_id not in all_recipes:
continue
recipe = all_recipes[r_id]
d_idx = s.get("day_of_week", 0)
day_str = day_names[d_idx] if 0 <= d_idx < 7 else f"Tag {d_idx}"
m_type = s.get("meal_type", "")
m_label = meal_labels.get(m_type, m_type.capitalize())
slot_key = f"{d_idx}_{m_type}_{r_id}"
if slot_key in seen_slots:
continue
seen_slots.add(slot_key)
slot_label = f"{day_str} {m_label}"
parsed = parse_ingredients_block(recipe.get("ingredients", ""))
items = []
for line in parsed:
if line.get("is_header"):
continue
raw_text = line.get("raw", "").strip()
if not raw_text:
continue
name = line.get("name") or raw_text
amt = str(line.get("amount") or "") if line.get("amount") is not None else ""
unit = line.get("unit") or ""
items.append({
"recipe_id": recipe["id"],
"recipe_title": recipe["title"],
"slot_label": slot_label,
"raw": raw_text,
"name": name,
"amount": amt,
"unit": unit
})
if items:
recipe_groups.append({
"recipe_id": recipe["id"],
"recipe_title": recipe["title"],
"slot_label": slot_label,
"items": items
})
return recipe_groups