UI & Features: Einkaufsliste im Kochbuch-Design, Supermarkt-Bereiche mit Laufweg-Sortierung, Textumbruch & schlanker Header

This commit is contained in:
2026-10-15 17:31:29 +02:00
parent 56d6512461
commit 111966c3ae
4 changed files with 1521 additions and 298 deletions
+302 -9
View File
@@ -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
View File
@@ -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
# ==========================================
+1 -1
View File
@@ -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 {
File diff suppressed because it is too large Load Diff