Feature: Einstiegsseite, Einkaufsliste, Wochenplan-Live-Suche & Zufallsgenerator-Optimierung
This commit is contained in:
+173
-1
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
+134
-5
@@ -41,7 +41,15 @@ from .database import (
|
||||
get_meal_plan,
|
||||
set_meal_plan_slot,
|
||||
delete_meal_plan_slot,
|
||||
clear_meal_plan_week
|
||||
clear_meal_plan_week,
|
||||
get_shopping_list,
|
||||
add_shopping_item,
|
||||
add_shopping_items_bulk,
|
||||
toggle_shopping_item,
|
||||
delete_shopping_item,
|
||||
delete_checked_shopping_items,
|
||||
clear_shopping_list,
|
||||
get_meal_plan_ingredients
|
||||
)
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -172,6 +180,15 @@ class MealPlanSlotRequest(BaseModel):
|
||||
recipe_id: Optional[int] = None
|
||||
custom_text: Optional[str] = None
|
||||
|
||||
class ShoppingItemCreate(BaseModel):
|
||||
item_name: str
|
||||
amount: Optional[str] = ""
|
||||
unit: Optional[str] = ""
|
||||
source: Optional[str] = ""
|
||||
|
||||
class ShoppingBulkCreate(BaseModel):
|
||||
items: List[ShoppingItemCreate]
|
||||
|
||||
|
||||
# ==========================================
|
||||
# AUTH VIEWS
|
||||
@@ -238,10 +255,43 @@ async def logout():
|
||||
# VIEWS
|
||||
# ==========================================
|
||||
|
||||
@app.get("/")
|
||||
async def index_view():
|
||||
"""Hauptroute leitet direkt auf das Kochbuch um"""
|
||||
return RedirectResponse(url="/cookbook", status_code=302)
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def home_view(request: Request):
|
||||
"""Einstiegsseite: Ausgeklapptes Menü / Hub"""
|
||||
all_recipes = get_all_recipes()
|
||||
today = date.today()
|
||||
iso_year, iso_week, _ = today.isocalendar()
|
||||
shopping_items = get_shopping_list()
|
||||
open_shopping_count = sum(1 for it in shopping_items if not it.get("checked"))
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="home.html",
|
||||
context={
|
||||
"recipe_count": len(all_recipes),
|
||||
"current_year": iso_year,
|
||||
"current_week": iso_week,
|
||||
"open_shopping_count": open_shopping_count,
|
||||
"chapters": CHAPTERS
|
||||
}
|
||||
)
|
||||
|
||||
@app.get("/shopping-list", response_class=HTMLResponse)
|
||||
@app.get("/groceries", response_class=HTMLResponse)
|
||||
async def shopping_list_view(request: Request):
|
||||
"""Einkaufsliste Übersicht"""
|
||||
items = get_shopping_list()
|
||||
today = date.today()
|
||||
iso_year, iso_week, _ = today.isocalendar()
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="shopping_list.html",
|
||||
context={
|
||||
"items": items,
|
||||
"current_year": iso_year,
|
||||
"current_week": iso_week
|
||||
}
|
||||
)
|
||||
|
||||
@app.get("/cookbook", response_class=HTMLResponse)
|
||||
async def cookbook_view(
|
||||
@@ -854,3 +904,82 @@ def api_clear_mealplan_week(year: int = Query(...), week: int = Query(...)):
|
||||
count = clear_meal_plan_week(year, week)
|
||||
return {"status": "ok", "cleared_count": count}
|
||||
|
||||
|
||||
# ==========================================
|
||||
# SHOPPING LIST API
|
||||
# ==========================================
|
||||
|
||||
@app.get("/api/shopping-list")
|
||||
def api_get_shopping_list():
|
||||
items = get_shopping_list()
|
||||
return {"status": "ok", "items": items}
|
||||
|
||||
|
||||
@app.post("/api/shopping-list")
|
||||
def api_add_shopping_item(payload: ShoppingItemCreate):
|
||||
if not payload.item_name.strip():
|
||||
raise HTTPException(status_code=400, detail="Name des Eintrags darf nicht leer sein.")
|
||||
item = add_shopping_item(
|
||||
item_name=payload.item_name,
|
||||
amount=payload.amount or "",
|
||||
unit=payload.unit or "",
|
||||
source=payload.source or ""
|
||||
)
|
||||
return {"status": "ok", "item": item}
|
||||
|
||||
|
||||
@app.post("/api/shopping-list/bulk")
|
||||
def api_add_shopping_bulk(payload: ShoppingBulkCreate):
|
||||
items_dicts = [it.model_dump() for it in payload.items]
|
||||
count = add_shopping_items_bulk(items_dicts)
|
||||
return {"status": "ok", "added_count": count}
|
||||
|
||||
|
||||
@app.post("/api/shopping-list/{item_id}/toggle")
|
||||
def api_toggle_shopping_item(item_id: int):
|
||||
updated = toggle_shopping_item(item_id)
|
||||
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()
|
||||
return {"status": "ok", "deleted_count": count}
|
||||
|
||||
|
||||
@app.delete("/api/shopping-list/all")
|
||||
def api_clear_shopping_list():
|
||||
count = clear_shopping_list()
|
||||
return {"status": "ok", "cleared_count": count}
|
||||
|
||||
|
||||
@app.delete("/api/shopping-list/{item_id}")
|
||||
def api_delete_shopping_item(item_id: int):
|
||||
success = delete_shopping_item(item_id)
|
||||
return {"status": "ok", "deleted": success}
|
||||
|
||||
|
||||
# ==========================================
|
||||
# MEAL PLAN INGREDIENTS API
|
||||
# ==========================================
|
||||
|
||||
@app.get("/api/mealplan/ingredients")
|
||||
def api_get_mealplan_ingredients(
|
||||
year: Optional[int] = Query(None),
|
||||
week: Optional[int] = Query(None)
|
||||
):
|
||||
today = date.today()
|
||||
iso_year, iso_week, _ = today.isocalendar()
|
||||
y = year or iso_year
|
||||
w = week or iso_week
|
||||
groups = get_meal_plan_ingredients(y, w)
|
||||
return {
|
||||
"status": "ok",
|
||||
"year": y,
|
||||
"week": w,
|
||||
"groups": groups
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2120,4 +2120,198 @@ body.modal-open {
|
||||
|
||||
/* Hinweis: A4 Druckregeln befinden sich zentral in app/templates/cookbook.html (<style id="print-engine-styles">) */
|
||||
|
||||
/* ==========================================
|
||||
ZURÜCK ZUM SHUFFLE FLOATING BANNER
|
||||
========================================== */
|
||||
.shuffle-return-banner {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--rostrot, #c53030);
|
||||
color: #ffffff;
|
||||
border-radius: 50px;
|
||||
padding: 6px 14px 6px 18px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
border: 2px solid #ffffff;
|
||||
}
|
||||
|
||||
.shuffle-return-banner:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.shuffle-return-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.shuffle-return-dismiss {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 1.15rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 50%;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.shuffle-return-dismiss:hover {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
SLOT-EDITOR REZEPT LIVE-SUCHE
|
||||
========================================== */
|
||||
.slot-recipe-search-container {
|
||||
position: relative;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.slot-search-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slot-search-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
border: 2px solid var(--tannengruen);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
z-index: 2400;
|
||||
padding: 4px 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.slot-search-item {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
|
||||
.slot-search-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.slot-search-item:hover,
|
||||
.slot-search-item.is-selected {
|
||||
background: rgba(30, 77, 59, 0.08);
|
||||
}
|
||||
|
||||
.slot-search-item-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--schwarz);
|
||||
}
|
||||
|
||||
.slot-search-item-meta {
|
||||
font-size: 0.78rem;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
WOCHENPLAN ZUTATEN CHECKLISTE
|
||||
========================================== */
|
||||
.mpi-group {
|
||||
margin-bottom: 16px;
|
||||
background: rgba(30, 77, 59, 0.04);
|
||||
border: 1.5px solid rgba(30, 77, 59, 0.15);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.mpi-group-header {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--tannengruen);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mpi-group-badge {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background: var(--tannengruen);
|
||||
color: var(--creme);
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.mpi-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(34, 34, 34, 0.1);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
|
||||
.mpi-item:hover {
|
||||
background: rgba(30, 77, 59, 0.05);
|
||||
border-color: var(--tannengruen);
|
||||
}
|
||||
|
||||
.mpi-item.is-unchecked {
|
||||
opacity: 0.55;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.mpi-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--tannengruen);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mpi-item-name {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mpi-item-qty {
|
||||
font-weight: 700;
|
||||
color: var(--tannengruen);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+589
-61
@@ -488,6 +488,12 @@
|
||||
|
||||
<!-- Desktop Buttons (auf Bildschirmen > 768px) -->
|
||||
<div class="cb-nav-actions desktop-nav-actions">
|
||||
<a href="/" class="cb-nav-btn" title="Menü-Übersicht öffnen">
|
||||
<span>🏠</span> <span>Menü</span>
|
||||
</a>
|
||||
<a href="/shopping-list" class="cb-nav-btn" title="Einkaufsliste öffnen">
|
||||
<span>🛒</span> <span>Einkaufsliste</span>
|
||||
</a>
|
||||
<button type="button" onclick="openMealPlanModal(event)" class="cb-nav-btn" title="Wochenmenü-Plan öffnen">
|
||||
<span>📅</span> <span>Wochenplan</span>
|
||||
</button>
|
||||
@@ -511,6 +517,12 @@
|
||||
<span>⋮</span>
|
||||
</button>
|
||||
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
|
||||
<a href="/" class="cb-mobile-item">
|
||||
<span>🏠</span> <span>Menü-Übersicht</span>
|
||||
</a>
|
||||
<a href="/shopping-list" class="cb-mobile-item">
|
||||
<span>🛒</span> <span>Einkaufsliste</span>
|
||||
</a>
|
||||
<button type="button" onclick="handleMobileMenuAction(openMealPlanModal)" class="cb-mobile-item">
|
||||
<span>📅</span> <span>Wochenmenü-Plan</span>
|
||||
</button>
|
||||
@@ -873,13 +885,27 @@
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Floating Return Banner: Zurück zum Zufallsgenerator (wenn von dort weitergeleitet) -->
|
||||
<div id="shuffle-return-banner" class="shuffle-return-banner no-print" style="display: none;">
|
||||
<button type="button" class="shuffle-return-btn" onclick="returnToShuffle()">
|
||||
<span style="font-size: 1.2rem;">🎲</span>
|
||||
<span>Zurück zum Zufallsgenerator</span>
|
||||
</button>
|
||||
<button type="button" class="shuffle-return-dismiss" onclick="dismissShuffleBanner()" title="Banner ausblenden">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL 0: WOCHENMENÜ-PLAN (no-print) -->
|
||||
<!-- ========================================== -->
|
||||
<div id="modal-mealplan" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-mealplan')">
|
||||
<div class="cb-modal-card" style="max-width: 960px;">
|
||||
<div class="cb-modal-header">
|
||||
<h3 class="cb-modal-title"><span>📅</span> Wochenmenü-Plan</h3>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<h3 class="cb-modal-title" style="margin: 0;"><span>📅</span> Wochenmenü-Plan</h3>
|
||||
<button type="button" id="mp-btn-back-to-shuffle" class="cb-btn cb-btn-outline" onclick="returnToShuffleFromMealPlan()" style="display: none; color: var(--rostrot); border-color: var(--rostrot); font-size: 0.82rem; padding: 4px 10px;">
|
||||
🎲 Zurück zum Shuffle
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="cb-modal-close" onclick="closeMealPlanModal()" title="Schließen">×</button>
|
||||
</div>
|
||||
<div class="cb-modal-body">
|
||||
@@ -904,7 +930,10 @@
|
||||
<span id="mp-today-pill" class="mealplan-today-tag" style="display: none;">Aktuelle Woche</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 6px;">
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button type="button" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme); font-size: 0.85rem; padding: 5px 12px; display: inline-flex; align-items: center; gap: 6px;" onclick="openMealPlanIngredientsModal()" title="Zutaten dieser Woche auf die Einkaufsliste setzen">
|
||||
<span>🛒</span> <span>Zutaten auf Einkaufsliste</span>
|
||||
</button>
|
||||
<button type="button" class="cb-btn cb-btn-outline" style="color: #666; border-color: #ccc; font-size: 0.85rem; padding: 5px 10px;" onclick="clearCurrentMealPlanWeek()" title="Ganze Woche leeren">
|
||||
🗑️ Woche leeren
|
||||
</button>
|
||||
@@ -931,7 +960,7 @@
|
||||
<input type="hidden" id="se-day">
|
||||
<input type="hidden" id="se-type" value="mittag">
|
||||
|
||||
<!-- Mahlzeittyp-Auswahl: Früh, Mittag, Abend -->
|
||||
<!-- Mahlzeittyp-Auswahl: Früh, Mittag, Kaffee, Abend -->
|
||||
<div style="margin-bottom: 14px;">
|
||||
<label class="cb-label">Mahlzeit:</label>
|
||||
<div class="slot-type-chips">
|
||||
@@ -942,18 +971,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 14px;">
|
||||
<label class="cb-label">Rezept aus dem Kochbuch wählen:</label>
|
||||
<select id="se-recipe-select" class="cb-select" onchange="onSlotRecipeSelect(this.value)">
|
||||
<option value="">-- Kein Rezept / Freitext nutzen --</option>
|
||||
</select>
|
||||
<!-- Live-Suchfeld für Rezepte statt starrer Dropdown -->
|
||||
<div class="slot-recipe-search-container">
|
||||
<label class="cb-label" for="se-recipe-search-input">Rezept aus dem Kochbuch suchen:</label>
|
||||
<div class="slot-search-input-wrapper">
|
||||
<input type="search" id="se-recipe-search-input" class="cb-input"
|
||||
placeholder="🔍 Rezept suchen (z. B. Waffeln, Chili, Suppe...)"
|
||||
oninput="onSlotRecipeSearchInput(this.value)"
|
||||
onfocus="onSlotRecipeSearchFocus()"
|
||||
autocomplete="off">
|
||||
<input type="hidden" id="se-recipe-id" value="">
|
||||
</div>
|
||||
|
||||
<!-- Badge für ausgewähltes Rezept -->
|
||||
<div id="se-selected-recipe-badge" style="display: none; margin-top: 8px; background: rgba(30, 77, 59, 0.08); border: 1.5px solid var(--tannengruen); border-radius: 6px; padding: 6px 12px; justify-content: space-between; align-items: center;">
|
||||
<span style="font-weight: 700; color: var(--tannengruen); font-size: 0.92rem;">
|
||||
✅ Ausgewählt: <span id="se-selected-recipe-name"></span>
|
||||
</span>
|
||||
<button type="button" onclick="clearSlotRecipeSelection()" style="background: transparent; border: none; color: #c53030; font-weight: 700; cursor: pointer; font-size: 0.85rem;">
|
||||
✕ Entfernen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Live Suchergebnisse Dropdown -->
|
||||
<div id="se-recipe-search-dropdown" class="slot-search-dropdown" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 10px 0; font-family: var(--font-hand); color: #888;">— ODER —</div>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label class="cb-label">Freitext (z. B. Restaurant, Reste, Brotzeit):</label>
|
||||
<input type="text" id="se-custom-text" class="cb-input" placeholder="z. B. Essen gehen, Brotzeit, Pizza" autocomplete="off">
|
||||
<input type="text" id="se-custom-text" class="cb-input" placeholder="z. B. Essen gehen, Brotzeit, Pizza" autocomplete="off" oninput="onSlotCustomTextInput(this.value)">
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 18px; padding-top: 12px; border-top: 1.5px dashed rgba(34,34,34,0.15); flex-wrap: wrap; gap: 8px;">
|
||||
@@ -978,6 +1026,94 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL: WOCHENPLAN ZUTATEN AUF EINKAUFSLISTE-->
|
||||
<!-- ========================================== -->
|
||||
<div id="modal-mealplan-ingredients" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-mealplan-ingredients')" style="z-index: 2300;">
|
||||
<div class="cb-modal-card" style="max-width: 640px;">
|
||||
<div class="cb-modal-header">
|
||||
<h3 class="cb-modal-title"><span>🛒</span> Zutaten für die Einkaufsliste</h3>
|
||||
<button type="button" class="cb-modal-close" onclick="closeMealPlanIngredientsModal()" title="Schließen">×</button>
|
||||
</div>
|
||||
<div class="cb-modal-body">
|
||||
<p style="font-size: 0.92rem; color: #555; margin: 0 0 12px 0;">
|
||||
Wähle die Zutaten der geplanten Gerichte für <strong id="mpi-week-label">diese Woche</strong> aus, die du auf die Einkaufsliste übertragen möchtest:
|
||||
</p>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding: 8px 12px; background: #fff; border: 1.5px solid rgba(34,34,34,0.15); border-radius: 8px;">
|
||||
<label style="display: flex; align-items: center; gap: 8px; font-size: 0.9rem; font-weight: 700; cursor: pointer; color: var(--tannengruen); margin: 0;">
|
||||
<input type="checkbox" id="mpi-toggle-all" checked onchange="toggleAllMpiCheckboxes(this.checked)" style="width: 18px; height: 18px; accent-color: var(--tannengruen);">
|
||||
<span>Alle auswählen / abwählen</span>
|
||||
</label>
|
||||
<span id="mpi-total-count" style="font-size: 0.85rem; color: #666; font-weight: 600;">0 Zutaten</span>
|
||||
</div>
|
||||
|
||||
<!-- Scrollbare Zutaten-Liste -->
|
||||
<div id="mpi-ingredients-container" style="max-height: 380px; overflow-y: auto; padding-right: 4px;">
|
||||
<!-- Dynamisch via JS -->
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 18px; padding-top: 12px; border-top: 1.5px dashed rgba(34,34,34,0.15); flex-wrap: wrap; gap: 8px;">
|
||||
<button type="button" onclick="closeMealPlanIngredientsModal()" class="cb-btn cb-btn-modal-cancel">
|
||||
Abbrechen
|
||||
</button>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<a href="/shopping-list" class="cb-btn cb-btn-outline" style="color: var(--schwarz); border-color: rgba(34,34,34,0.3); font-size: 0.9rem;">
|
||||
<span>🛒</span> Zur Einkaufsliste
|
||||
</a>
|
||||
<button type="button" id="mpi-btn-transfer" onclick="transferMpiToShoppingList()" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme); font-weight: 700; font-size: 0.95rem; padding: 8px 18px;">
|
||||
<span>➕</span> <span>Ausgewählte übertragen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL: SHUFFLE-GERICHT IN WOCHENPLAN -->
|
||||
<!-- ========================================== -->
|
||||
<div id="modal-shuffle-assign" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-shuffle-assign')" style="z-index: 2250;">
|
||||
<div class="cb-modal-card" style="max-width: 440px;">
|
||||
<div class="cb-modal-header">
|
||||
<h4 class="cb-modal-title"><span>📅</span> In Wochenplan einfügen</h4>
|
||||
<button type="button" class="cb-modal-close" onclick="closeShuffleAssignModal()">×</button>
|
||||
</div>
|
||||
<div class="cb-modal-body">
|
||||
<input type="hidden" id="sa-recipe-id">
|
||||
<div style="margin-bottom: 14px; background: rgba(30,77,59,0.06); padding: 10px 14px; border-radius: 8px; border: 1.5px solid rgba(30,77,59,0.2);">
|
||||
<span style="font-size: 0.8rem; color: #666; display: block; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700;">Rezept:</span>
|
||||
<span id="sa-recipe-title" style="font-size: 1.15rem; font-weight: 700; color: var(--tannengruen); display: block; margin-top: 2px;"></span>
|
||||
</div>
|
||||
<div class="cb-form-group">
|
||||
<label class="cb-label">Wochentag wählen:</label>
|
||||
<select id="sa-day-select" class="cb-select">
|
||||
<option value="0">Montag</option>
|
||||
<option value="1">Dienstag</option>
|
||||
<option value="2">Mittwoch</option>
|
||||
<option value="3">Donnerstag</option>
|
||||
<option value="4">Freitag</option>
|
||||
<option value="5">Samstag</option>
|
||||
<option value="6">Sonntag</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="cb-form-group">
|
||||
<label class="cb-label">Mahlzeit:</label>
|
||||
<select id="sa-meal-type-select" class="cb-select">
|
||||
<option value="mittag" selected>☀️ Mittag</option>
|
||||
<option value="abend">🌙 Abend</option>
|
||||
<option value="kaffee">☕ Kaffee & Kuchen</option>
|
||||
<option value="frueh">🌅 Früh</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; padding-top: 12px; border-top: 1px solid #ddd;">
|
||||
<button type="button" onclick="closeShuffleAssignModal()" class="cb-btn cb-btn-modal-cancel">Abbrechen</button>
|
||||
<button type="button" onclick="saveShuffleToMealPlan()" class="cb-btn cb-btn-modal-submit">💾 Eintragen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL 1: ESSENS-ZUFALLSGENERATOR (no-print)-->
|
||||
<!-- ========================================== -->
|
||||
@@ -1438,11 +1574,15 @@
|
||||
if (modalId === 'modal-generator') closeGeneratorModal();
|
||||
if (modalId === 'modal-recipe') closeRecipeModal();
|
||||
if (modalId === 'modal-print') closePrintModal();
|
||||
if (modalId === 'modal-mealplan-ingredients') closeMealPlanIngredientsModal();
|
||||
if (modalId === 'modal-shuffle-assign') closeShuffleAssignModal();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeShuffleAssignModal();
|
||||
closeMealPlanIngredientsModal();
|
||||
closeSlotEditor();
|
||||
closeMealPlanModal();
|
||||
closeGeneratorModal();
|
||||
@@ -1476,6 +1616,10 @@
|
||||
];
|
||||
const TOTAL_RECIPES = {{ total_recipes }};
|
||||
|
||||
// Filter für Zufallsgenerator: Kategorien Desserts (9), Kuchen & Gebäck (10) und Plätzchen (11) ausschließen
|
||||
const SHUFFLE_CHAPTERS = GEN_CHAPTERS.filter(c => c.number !== 9 && c.number !== 10 && c.number !== 11);
|
||||
const SHUFFLE_TOTAL_RECIPES = SHUFFLE_CHAPTERS.reduce((s, c) => s + (c.count || 0), 0);
|
||||
|
||||
let genSlotCount = 2;
|
||||
let genSlots = [
|
||||
{ category: "", efforts: [] },
|
||||
@@ -1486,15 +1630,55 @@
|
||||
];
|
||||
let genCurrentResults = [];
|
||||
|
||||
function saveShuffleStateToStorage() {
|
||||
try {
|
||||
sessionStorage.setItem('wtc_shuffle_results', JSON.stringify(genCurrentResults || []));
|
||||
sessionStorage.setItem('wtc_shuffle_slots', JSON.stringify(genSlots || []));
|
||||
sessionStorage.setItem('wtc_shuffle_count', String(genSlotCount));
|
||||
} catch (e) {
|
||||
console.warn('Fehler beim Speichern des Shuffle-Zustands:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreShuffleStateFromStorage() {
|
||||
try {
|
||||
const storedResults = sessionStorage.getItem('wtc_shuffle_results');
|
||||
const storedSlots = sessionStorage.getItem('wtc_shuffle_slots');
|
||||
const storedCount = sessionStorage.getItem('wtc_shuffle_count');
|
||||
|
||||
if (storedSlots) {
|
||||
genSlots = JSON.parse(storedSlots);
|
||||
}
|
||||
if (storedCount) {
|
||||
genSlotCount = parseInt(storedCount, 10) || 2;
|
||||
document.querySelectorAll('.gen-count-btn').forEach((btn, idx) => {
|
||||
btn.classList.toggle('active', idx + 1 === genSlotCount);
|
||||
});
|
||||
}
|
||||
if (storedResults) {
|
||||
genCurrentResults = JSON.parse(storedResults);
|
||||
if (genCurrentResults.length > 0) {
|
||||
renderGenResults(false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Fehler beim Wiederherstellen des Shuffle-Zustands:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function setGenSlotCount(count) {
|
||||
genSlotCount = count;
|
||||
document.querySelectorAll('.gen-count-btn').forEach((btn, idx) => {
|
||||
btn.classList.toggle('active', idx + 1 === count);
|
||||
});
|
||||
renderGenSlots();
|
||||
saveShuffleStateToStorage();
|
||||
}
|
||||
|
||||
function initGeneratorSlots() {
|
||||
if (genCurrentResults.length === 0 && sessionStorage.getItem('wtc_shuffle_results')) {
|
||||
restoreShuffleStateFromStorage();
|
||||
}
|
||||
renderGenSlots();
|
||||
}
|
||||
|
||||
@@ -1503,7 +1687,7 @@
|
||||
let html = '';
|
||||
for (let i = 0; i < genSlotCount; i++) {
|
||||
const slot = genSlots[i];
|
||||
const selectedChap = slot.category ? GEN_CHAPTERS.find(c => c.number == slot.category) : null;
|
||||
const selectedChap = slot.category ? SHUFFLE_CHAPTERS.find(c => c.number == slot.category) : null;
|
||||
const isAllEfforts = !slot.efforts || slot.efforts.length === 0;
|
||||
const isGering = slot.efforts && slot.efforts.includes('gering');
|
||||
const isMittel = slot.efforts && slot.efforts.includes('mittel');
|
||||
@@ -1515,7 +1699,7 @@
|
||||
<span>🍽️ Gericht ${i + 1}</span>
|
||||
</div>
|
||||
<div class="cb-row" style="gap: 12px; align-items: flex-start;">
|
||||
<!-- Kapitel Dropdown ohne "Kapitel X" Präfix -->
|
||||
<!-- Kapitel Dropdown ohne "Kapitel X" Präfix & ohne Desserts/Kuchen -->
|
||||
<div class="cb-col" style="flex: 1.6; min-width: 180px;">
|
||||
<label style="font-size: 0.85rem; color: #555; font-weight: 700; margin-bottom: 4px; display: block;">Kategorie / Kapitel:</label>
|
||||
<div class="custom-chapter-select" id="custom-chapter-select-${i}">
|
||||
@@ -1526,7 +1710,7 @@
|
||||
<span class="chapter-select-btn-title">${selectedChap.title}</span>
|
||||
` : `
|
||||
<span style="font-size: 1.25rem; line-height: 1;">🎲</span>
|
||||
<span class="chapter-select-btn-title">Alle Kapitel (Beliebig)</span>
|
||||
<span class="chapter-select-btn-title">Alle Hauptgerichte (Beliebig)</span>
|
||||
`}
|
||||
</div>
|
||||
<span class="chapter-select-arrow">▼</span>
|
||||
@@ -1535,12 +1719,12 @@
|
||||
<div class="chapter-option ${!slot.category ? 'is-selected' : ''}" onclick="selectSlotChapter(${i}, '')">
|
||||
<span style="font-size: 1.3rem; line-height: 1; width: 32px; text-align: center;">🎲</span>
|
||||
<div class="chapter-option-text">
|
||||
<div class="chapter-option-title">Alle Kapitel / Beliebig</div>
|
||||
<div style="font-size: 0.8rem; color: #666;">Zufall aus dem gesamten Kochbuch</div>
|
||||
<div class="chapter-option-title">Alle Hauptgerichte / Beliebig</div>
|
||||
<div style="font-size: 0.8rem; color: #666;">Zufall aus allen herzhaften Kategorien</div>
|
||||
</div>
|
||||
<span class="chapter-recipe-count">${TOTAL_RECIPES} Rezepte</span>
|
||||
<span class="chapter-recipe-count">${SHUFFLE_TOTAL_RECIPES} Rezepte</span>
|
||||
</div>
|
||||
${GEN_CHAPTERS.map(c => `
|
||||
${SHUFFLE_CHAPTERS.map(c => `
|
||||
<div class="chapter-option ${slot.category == c.number ? 'is-selected' : ''}" onclick="selectSlotChapter(${i}, ${c.number})">
|
||||
<img src="${c.illustration}" class="chapter-option-thumb" alt="${c.title}">
|
||||
<div class="chapter-option-text">
|
||||
@@ -1682,11 +1866,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderGenResults() {
|
||||
function renderGenResults(saveState = true) {
|
||||
const sec = document.getElementById('gen-results-section');
|
||||
const list = document.getElementById('gen-results-list');
|
||||
sec.style.display = 'block';
|
||||
|
||||
if (saveState) {
|
||||
saveShuffleStateToStorage();
|
||||
}
|
||||
|
||||
if (!genCurrentResults || genCurrentResults.length === 0) {
|
||||
list.innerHTML = '<p style="text-align:center; color:#666;">Keine Rezepte ausgelost.</p>';
|
||||
return;
|
||||
@@ -1743,11 +1931,15 @@
|
||||
🔄 Neu
|
||||
</button>
|
||||
</div>
|
||||
<div class="gen-actions">
|
||||
<div class="gen-actions" style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<a href="#rezept-${d.id}" onclick="jumpToRecipe(${d.id}, event)" class="cb-btn cb-btn-creme"
|
||||
style="background: var(--tannengruen); color: var(--creme); font-size: 0.95rem; padding: 6px 16px;">
|
||||
📖 Direkt zum Rezept im Buch
|
||||
</a>
|
||||
<button type="button" onclick="openShuffleToMealPlanModal(${d.id}, '${escapeHtml(d.title || d.name)}', event)" class="cb-btn cb-btn-outline"
|
||||
style="color: var(--tannengruen); border-color: var(--tannengruen); font-size: 0.95rem; padding: 6px 14px; display: inline-flex; align-items: center; gap: 6px;">
|
||||
<span>📅</span> <span>In Wochenplan</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1756,6 +1948,8 @@
|
||||
|
||||
function jumpToRecipe(recipeId, e) {
|
||||
if (e) e.preventDefault();
|
||||
sessionStorage.setItem('wtc_from_shuffle', 'true');
|
||||
checkShuffleReturnState();
|
||||
closeGeneratorModal();
|
||||
const el = document.getElementById(`rezept-${recipeId}`);
|
||||
if (el) {
|
||||
@@ -1768,6 +1962,114 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SHUFFLE RETENTION & WOCHENPLAN ÜBERTRAGUNG
|
||||
// ==========================================
|
||||
function checkShuffleReturnState() {
|
||||
const isFromShuffle = sessionStorage.getItem('wtc_from_shuffle') === 'true';
|
||||
const hasResults = !!sessionStorage.getItem('wtc_shuffle_results');
|
||||
const banner = document.getElementById('shuffle-return-banner');
|
||||
const mpBtn = document.getElementById('mp-btn-back-to-shuffle');
|
||||
|
||||
if (isFromShuffle && hasResults) {
|
||||
if (banner) banner.style.display = 'flex';
|
||||
if (mpBtn) mpBtn.style.display = 'inline-flex';
|
||||
} else {
|
||||
if (banner) banner.style.display = 'none';
|
||||
if (mpBtn) mpBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function returnToShuffle() {
|
||||
closeMealPlanIngredientsModal();
|
||||
closeShuffleAssignModal();
|
||||
closeSlotEditor();
|
||||
closeMealPlanModal();
|
||||
closeRecipeModal();
|
||||
closePrintModal();
|
||||
|
||||
openGeneratorModal();
|
||||
restoreShuffleStateFromStorage();
|
||||
renderGenSlots();
|
||||
}
|
||||
|
||||
function returnToShuffleFromMealPlan() {
|
||||
closeMealPlanModal();
|
||||
returnToShuffle();
|
||||
}
|
||||
|
||||
function dismissShuffleBanner() {
|
||||
sessionStorage.removeItem('wtc_from_shuffle');
|
||||
checkShuffleReturnState();
|
||||
}
|
||||
|
||||
function openShuffleToMealPlanModal(recipeId, recipeTitle, e) {
|
||||
if (e) e.stopPropagation();
|
||||
document.getElementById('sa-recipe-id').value = recipeId;
|
||||
document.getElementById('sa-recipe-title').textContent = recipeTitle || 'Rezept';
|
||||
|
||||
const jsDay = new Date().getDay();
|
||||
const isoDay = jsDay === 0 ? 6 : jsDay - 1;
|
||||
document.getElementById('sa-day-select').value = String(isoDay);
|
||||
document.getElementById('sa-meal-type-select').value = 'mittag';
|
||||
|
||||
document.getElementById('modal-shuffle-assign').classList.add('active');
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
function closeShuffleAssignModal() {
|
||||
document.getElementById('modal-shuffle-assign').classList.remove('active');
|
||||
if (!document.getElementById('modal-generator').classList.contains('active') &&
|
||||
!document.getElementById('modal-mealplan').classList.contains('active')) {
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveShuffleToMealPlan() {
|
||||
const recipeId = parseInt(document.getElementById('sa-recipe-id').value, 10);
|
||||
const dayIdx = parseInt(document.getElementById('sa-day-select').value, 10);
|
||||
const mealType = document.getElementById('sa-meal-type-select').value;
|
||||
const dayName = document.getElementById('sa-day-select').options[document.getElementById('sa-day-select').selectedIndex].text;
|
||||
const mealName = document.getElementById('sa-meal-type-select').options[document.getElementById('sa-meal-type-select').selectedIndex].text;
|
||||
|
||||
let year = mpCurrentYear;
|
||||
let week = mpCurrentWeek;
|
||||
if (!year || !week) {
|
||||
const now = new Date();
|
||||
const d = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
week = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||
year = d.getUTCFullYear();
|
||||
}
|
||||
|
||||
const payload = {
|
||||
year: year,
|
||||
week: week,
|
||||
day_of_week: dayIdx,
|
||||
meal_type: mealType,
|
||||
recipe_id: recipeId,
|
||||
custom_text: null
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/mealplan/slot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) throw new Error('Speichern im Wochenplan fehlgeschlagen');
|
||||
|
||||
closeShuffleAssignModal();
|
||||
sessionStorage.setItem('wtc_from_shuffle', 'true');
|
||||
checkShuffleReturnState();
|
||||
showToast(`📅 In Wochenplan eingetragen (${dayName}, ${mealName})!`);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// REZEPT-MODAL FORM SUBMIT & BILD-KOMPRIMIERUNG
|
||||
// ==========================================
|
||||
@@ -2022,7 +2324,9 @@
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// Slot Editor
|
||||
// ==========================================
|
||||
// SLOT EDITOR & REZEPT LIVE-SUCHE
|
||||
// ==========================================
|
||||
function openSlotEditor(dayIndex, mealType, dayName) {
|
||||
document.getElementById('se-day').value = dayIndex;
|
||||
|
||||
@@ -2037,9 +2341,6 @@
|
||||
|
||||
document.getElementById('slot-editor-title').textContent = `🍽️ ${dayName || 'Tag'}: Mahlzeit planen`;
|
||||
|
||||
// Rezept-Dropdown initialisieren
|
||||
populateSlotRecipeDropdown();
|
||||
|
||||
// Mahlzeittyp setzen & Felder füllen
|
||||
selectSlotMealType(mealType, false);
|
||||
|
||||
@@ -2052,11 +2353,7 @@
|
||||
|
||||
// Aktiven Chip hervorheben
|
||||
document.querySelectorAll('.slot-type-chip').forEach(btn => {
|
||||
if (btn.getAttribute('data-type') === mealType) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
btn.classList.toggle('active', btn.getAttribute('data-type') === mealType);
|
||||
});
|
||||
|
||||
// Bestehende Werte für diesen Tag + mealType prüfen
|
||||
@@ -2064,48 +2361,123 @@
|
||||
const slotKey = `${dayIndex}_${mealType}`;
|
||||
const current = mpData && mpData.slots ? mpData.slots[slotKey] : null;
|
||||
|
||||
const sel = document.getElementById('se-recipe-select');
|
||||
const customInput = document.getElementById('se-custom-text');
|
||||
const delBtn = document.getElementById('se-btn-delete');
|
||||
|
||||
if (current && (current.recipe_title || (current.custom_text && current.custom_text.trim()))) {
|
||||
sel.value = current.recipe_id || '';
|
||||
if (current.recipe_id && current.recipe_title) {
|
||||
selectSlotRecipe(current.recipe_id, current.recipe_title);
|
||||
} else {
|
||||
clearSlotRecipeSelection();
|
||||
}
|
||||
customInput.value = current.custom_text || '';
|
||||
if (delBtn) delBtn.style.display = 'inline-block';
|
||||
} else {
|
||||
if (userClicked) {
|
||||
sel.value = '';
|
||||
clearSlotRecipeSelection();
|
||||
customInput.value = '';
|
||||
}
|
||||
if (delBtn) delBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function populateSlotRecipeDropdown() {
|
||||
const sel = document.getElementById('se-recipe-select');
|
||||
if (sel.options.length > 1) return; // bereits befüllt
|
||||
function onSlotRecipeSearchInput(val) {
|
||||
const dropdown = document.getElementById('se-recipe-search-dropdown');
|
||||
const term = (val || '').trim().toLowerCase();
|
||||
|
||||
sel.innerHTML = '<option value="">-- Kein Rezept / Freitext nutzen --</option>';
|
||||
if (mpData && mpData.recipes) {
|
||||
const cats = {};
|
||||
mpData.recipes.forEach(r => {
|
||||
if (!cats[r.category]) cats[r.category] = [];
|
||||
cats[r.category].push(r);
|
||||
});
|
||||
Object.keys(cats).sort().forEach(cat => {
|
||||
const optGroup = document.createElement('optgroup');
|
||||
optGroup.label = cat;
|
||||
cats[cat].sort((a,b) => a.title.localeCompare(b.title)).forEach(r => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = r.id;
|
||||
opt.textContent = `${r.title} (${r.effort})`;
|
||||
optGroup.appendChild(opt);
|
||||
});
|
||||
sel.appendChild(optGroup);
|
||||
});
|
||||
if (!mpData || !mpData.recipes) return;
|
||||
|
||||
let matches = [];
|
||||
if (!term) {
|
||||
matches = mpData.recipes.slice(0, 15);
|
||||
} else {
|
||||
matches = mpData.recipes.filter(r => {
|
||||
const titleMatch = (r.title || '').toLowerCase().includes(term);
|
||||
const catMatch = (r.category || '').toLowerCase().includes(term);
|
||||
return titleMatch || catMatch;
|
||||
}).slice(0, 20);
|
||||
}
|
||||
|
||||
renderSlotRecipeSearchResults(matches);
|
||||
}
|
||||
|
||||
function onSlotRecipeSearchFocus() {
|
||||
const input = document.getElementById('se-recipe-search-input');
|
||||
onSlotRecipeSearchInput(input.value);
|
||||
}
|
||||
|
||||
function renderSlotRecipeSearchResults(matches) {
|
||||
const dropdown = document.getElementById('se-recipe-search-dropdown');
|
||||
if (!matches || matches.length === 0) {
|
||||
dropdown.innerHTML = '<div style="padding: 10px; color: #888; text-align: center; font-size: 0.9rem;">Kein Rezept gefunden</div>';
|
||||
dropdown.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const effortBadge = (eff) => {
|
||||
if (eff === 'gering') return '🟢 Gering';
|
||||
if (eff === 'hoch') return '🔴 Hoch';
|
||||
return '🟡 Mittel';
|
||||
};
|
||||
|
||||
let html = '';
|
||||
matches.forEach(r => {
|
||||
html += `
|
||||
<div class="slot-search-item" onclick="selectSlotRecipe(${r.id}, '${escapeHtml(r.title)}')">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div class="slot-search-item-title">${escapeHtml(r.title)}</div>
|
||||
<div class="slot-search-item-meta">
|
||||
<span>${escapeHtml(r.category || '')}</span>
|
||||
<span>•</span>
|
||||
<span>${effortBadge(r.effort)}</span>
|
||||
${r.cook_time ? `<span>• ⏱️ ${escapeHtml(r.cook_time)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<span style="font-size: 0.85rem; color: var(--tannengruen); font-weight: 700; margin-left: 8px;">Wählen ➔</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
dropdown.innerHTML = html;
|
||||
dropdown.style.display = 'block';
|
||||
}
|
||||
|
||||
function selectSlotRecipe(recipeId, recipeTitle) {
|
||||
if (!recipeTitle && mpData && mpData.recipes) {
|
||||
const found = mpData.recipes.find(r => r.id === recipeId);
|
||||
if (found) recipeTitle = found.title;
|
||||
}
|
||||
|
||||
document.getElementById('se-recipe-id').value = recipeId;
|
||||
document.getElementById('se-recipe-search-input').value = recipeTitle || '';
|
||||
document.getElementById('se-selected-recipe-name').textContent = recipeTitle || '';
|
||||
document.getElementById('se-selected-recipe-badge').style.display = 'flex';
|
||||
document.getElementById('se-recipe-search-dropdown').style.display = 'none';
|
||||
|
||||
// Freitext leeren
|
||||
document.getElementById('se-custom-text').value = '';
|
||||
}
|
||||
|
||||
function clearSlotRecipeSelection() {
|
||||
document.getElementById('se-recipe-id').value = '';
|
||||
document.getElementById('se-recipe-search-input').value = '';
|
||||
document.getElementById('se-selected-recipe-badge').style.display = 'none';
|
||||
document.getElementById('se-recipe-search-dropdown').style.display = 'none';
|
||||
}
|
||||
|
||||
function onSlotCustomTextInput(val) {
|
||||
if (val && val.trim().length > 0) {
|
||||
clearSlotRecipeSelection();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.slot-recipe-search-container')) {
|
||||
const dropdown = document.getElementById('se-recipe-search-dropdown');
|
||||
if (dropdown) dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
function closeSlotEditor() {
|
||||
document.getElementById('modal-slot-editor').classList.remove('active');
|
||||
if (!document.getElementById('modal-mealplan').classList.contains('active')) {
|
||||
@@ -2113,25 +2485,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
function onSlotRecipeSelect(val) {
|
||||
if (val) {
|
||||
document.getElementById('se-custom-text').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function rollRandomRecipeForSlot() {
|
||||
if (!mpData || !mpData.recipes || mpData.recipes.length === 0) return;
|
||||
const rand = mpData.recipes[Math.floor(Math.random() * mpData.recipes.length)];
|
||||
const sel = document.getElementById('se-recipe-select');
|
||||
sel.value = rand.id;
|
||||
document.getElementById('se-custom-text').value = '';
|
||||
selectSlotRecipe(rand.id, rand.title);
|
||||
showToast(`🎲 Ausgewählt: ${rand.title}`);
|
||||
}
|
||||
|
||||
async function saveSlotEditor() {
|
||||
const dayIndex = parseInt(document.getElementById('se-day').value, 10);
|
||||
const mealType = document.getElementById('se-type').value;
|
||||
const recipeVal = document.getElementById('se-recipe-select').value;
|
||||
const recipeVal = document.getElementById('se-recipe-id').value;
|
||||
const customVal = document.getElementById('se-custom-text').value.trim();
|
||||
|
||||
const payload = {
|
||||
@@ -2189,6 +2553,170 @@
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// WOCHENPLAN ZUTATEN AUF EINKAUFSLISTE
|
||||
// ==========================================
|
||||
let mpiIngredientsData = [];
|
||||
|
||||
async function openMealPlanIngredientsModal() {
|
||||
if (!mpCurrentYear || !mpCurrentWeek) {
|
||||
showToast('Wochenplan noch nicht geladen');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('mpi-ingredients-container');
|
||||
const countBadge = document.getElementById('mpi-total-count');
|
||||
const weekLabel = document.getElementById('mpi-week-label');
|
||||
|
||||
weekLabel.textContent = `KW ${mpCurrentWeek}`;
|
||||
container.innerHTML = '<div style="text-align: center; padding: 30px; color: #777;"><span>⏳</span> Zutaten werden zusammengestellt...</div>';
|
||||
countBadge.textContent = 'Laden...';
|
||||
|
||||
document.getElementById('modal-mealplan-ingredients').classList.add('active');
|
||||
document.body.classList.add('modal-open');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/mealplan/ingredients?year=${mpCurrentYear}&week=${mpCurrentWeek}`);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail || 'Fehler beim Laden der Zutaten');
|
||||
|
||||
const groups = data.groups || [];
|
||||
mpiIngredientsData = groups;
|
||||
|
||||
if (groups.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div style="text-align: center; padding: 30px; color: #666;">
|
||||
<span style="font-size: 2.5rem; display: block; margin-bottom: 8px;">🍽️</span>
|
||||
<h4 style="margin: 0 0 6px 0; color: var(--tannengruen); font-family: var(--font-hand); font-size: 1.4rem;">Keine Rezepte mit Zutaten geplant</h4>
|
||||
<p style="font-size: 0.95rem; margin: 0;">Füge im Wochenplan Rezepte aus dem Kochbuch hinzu, um deren Zutaten hier automatisch auf deine Einkaufsliste zu setzen.</p>
|
||||
</div>
|
||||
`;
|
||||
countBadge.textContent = '0 Zutaten';
|
||||
return;
|
||||
}
|
||||
|
||||
let totalItemCount = 0;
|
||||
let html = '';
|
||||
|
||||
groups.forEach((grp, gIdx) => {
|
||||
totalItemCount += grp.items.length;
|
||||
html += `
|
||||
<div class="mpi-group">
|
||||
<div class="mpi-group-header">
|
||||
<span>📖 ${escapeHtml(grp.recipe_title)}</span>
|
||||
<span class="mpi-group-badge">${escapeHtml(grp.slot_label)}</span>
|
||||
</div>
|
||||
<div class="mpi-group-items">
|
||||
`;
|
||||
|
||||
grp.items.forEach((it, iIdx) => {
|
||||
const qtyStr = ((it.amount || '') + (it.unit ? ' ' + it.unit : '')).trim();
|
||||
const itemId = `mpi_cb_${gIdx}_${iIdx}`;
|
||||
html += `
|
||||
<label class="mpi-item" id="mpi-label-${itemId}">
|
||||
<input type="checkbox" class="mpi-checkbox" id="${itemId}" checked
|
||||
data-recipe="${escapeHtml(grp.recipe_title)}"
|
||||
data-name="${escapeHtml(it.name)}"
|
||||
data-amount="${escapeHtml(it.amount || '')}"
|
||||
data-unit="${escapeHtml(it.unit || '')}"
|
||||
onchange="onMpiItemToggle(this, '${itemId}')">
|
||||
<span class="mpi-item-name">${escapeHtml(it.name)}</span>
|
||||
${qtyStr ? `<span class="mpi-item-qty">${escapeHtml(qtyStr)}</span>` : ''}
|
||||
</label>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
countBadge.textContent = `${totalItemCount} ${totalItemCount === 1 ? 'Zutat' : 'Zutaten'}`;
|
||||
document.getElementById('mpi-toggle-all').checked = true;
|
||||
} catch (err) {
|
||||
container.innerHTML = `<div style="text-align: center; padding: 20px; color: #c53030;">❌ ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function closeMealPlanIngredientsModal() {
|
||||
document.getElementById('modal-mealplan-ingredients').classList.remove('active');
|
||||
if (!document.getElementById('modal-mealplan').classList.contains('active')) {
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAllMpiCheckboxes(checked) {
|
||||
document.querySelectorAll('.mpi-checkbox').forEach(cb => {
|
||||
cb.checked = checked;
|
||||
const parent = cb.closest('.mpi-item');
|
||||
if (parent) parent.classList.toggle('is-unchecked', !checked);
|
||||
});
|
||||
}
|
||||
|
||||
function onMpiItemToggle(cb, id) {
|
||||
const parent = cb.closest('.mpi-item');
|
||||
if (parent) parent.classList.toggle('is-unchecked', !cb.checked);
|
||||
}
|
||||
|
||||
async function transferMpiToShoppingList() {
|
||||
const btn = document.getElementById('mpi-btn-transfer');
|
||||
const checkedBoxes = Array.from(document.querySelectorAll('.mpi-checkbox:checked'));
|
||||
|
||||
if (checkedBoxes.length === 0) {
|
||||
alert('Bitte wähle mindestens eine Zutat aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span>⏳</span> <span>Wird übertragen...</span>';
|
||||
|
||||
const items = checkedBoxes.map(cb => ({
|
||||
item_name: cb.getAttribute('data-name'),
|
||||
amount: cb.getAttribute('data-amount') || '',
|
||||
unit: cb.getAttribute('data-unit') || '',
|
||||
source: `Wochenplan KW ${mpCurrentWeek} (${cb.getAttribute('data-recipe')})`
|
||||
}));
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/shopping-list/bulk', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: items })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail || 'Fehler beim Übertragen');
|
||||
|
||||
closeMealPlanIngredientsModal();
|
||||
showToast(`🛒 ${data.added_count} Zutaten auf die Einkaufsliste gesetzt!`);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>➕</span> <span>Ausgewählte übertragen</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// INITIALISIERUNG BEIM SEITENAUFRUF
|
||||
// ==========================================
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 1. Prüfe Rückkehr-Banner vom Shuffle
|
||||
checkShuffleReturnState();
|
||||
|
||||
// 2. Prüfe Query-Parameter open=...
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const openParam = urlParams.get('open');
|
||||
if (openParam === 'generator' || openParam === 'shuffle') {
|
||||
openGeneratorModal();
|
||||
} else if (openParam === 'mealplan' || openParam === 'wochenplan') {
|
||||
openMealPlanModal();
|
||||
} else if (openParam === 'new-recipe' || openParam === 'recipe') {
|
||||
openRecipeModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>What To Cook? – Hauptmenü</title>
|
||||
<link rel="stylesheet" href="/static/cookbook.css?v=4.5.4">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍽️</text></svg>">
|
||||
<style>
|
||||
/* Styling für das ausgeklappte Menü / Hub Dashboard */
|
||||
body.home-body {
|
||||
background-color: var(--grau-bg, #e6e2dc);
|
||||
background-image: radial-gradient(rgba(30, 77, 59, 0.06) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--schwarz);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.home-header {
|
||||
background-color: var(--creme);
|
||||
border-bottom: 2px solid var(--schwarz);
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.home-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.home-brand-icon {
|
||||
font-size: 2rem;
|
||||
filter: drop-shadow(1px 2px 2px rgba(0,0,0,0.15));
|
||||
}
|
||||
|
||||
.home-brand-title {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 1.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--tannengruen);
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-brand-sub {
|
||||
font-size: 0.82rem;
|
||||
color: #666;
|
||||
margin: 2px 0 0 0;
|
||||
}
|
||||
|
||||
.home-header-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.home-header-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(30, 77, 59, 0.1);
|
||||
color: var(--tannengruen);
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border: 1px solid rgba(30, 77, 59, 0.2);
|
||||
}
|
||||
|
||||
.home-logout-btn {
|
||||
color: #777;
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.home-logout-btn:hover {
|
||||
color: #b00;
|
||||
background: rgba(200, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Der eigentliche ausgeklappte Menü-Container */
|
||||
.home-container {
|
||||
max-width: 1080px;
|
||||
width: 100%;
|
||||
margin: 32px auto 48px auto;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Menü-Tafel Intro */
|
||||
.menu-board-intro {
|
||||
text-align: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.menu-board-tagline {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 1.35rem;
|
||||
color: var(--altrosa);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.menu-board-heading {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 2.8rem;
|
||||
color: var(--tannengruen);
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.menu-board-desc {
|
||||
font-size: 1rem;
|
||||
color: #555;
|
||||
max-width: 620px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Das ausgeklappte Speisekarten-Grid */
|
||||
.menu-spread {
|
||||
background: var(--creme);
|
||||
border: 3px solid var(--schwarz);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 36px rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.1);
|
||||
padding: 32px 28px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menu-spread::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px; left: 6px; right: 6px; bottom: 6px;
|
||||
border: 1.5px dashed rgba(30, 77, 59, 0.35);
|
||||
border-radius: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
|
||||
gap: 22px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Einzelne Menükarte */
|
||||
.menu-card {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 2px solid var(--schwarz);
|
||||
border-radius: 12px;
|
||||
padding: 24px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.12);
|
||||
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.2s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-card:hover {
|
||||
transform: translateY(-4px) scale(1.01);
|
||||
box-shadow: 6px 8px 0px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.menu-card.featured {
|
||||
background: #FFFDF0;
|
||||
border-color: var(--tannengruen);
|
||||
border-width: 2.5px;
|
||||
}
|
||||
|
||||
.menu-card-top {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.menu-card-icon-wrap {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
background: var(--creme);
|
||||
border: 2px solid var(--schwarz);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 2px 2px 0px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.menu-card-title {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 1.7rem;
|
||||
color: var(--tannengruen);
|
||||
margin: 0 0 6px 0;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.menu-card-desc {
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.45;
|
||||
color: #4a453f;
|
||||
margin: 0 0 14px 0;
|
||||
}
|
||||
|
||||
.menu-card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.menu-tag {
|
||||
font-size: 0.76rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
background: rgba(30, 77, 59, 0.08);
|
||||
color: var(--tannengruen);
|
||||
border: 1px solid rgba(30, 77, 59, 0.2);
|
||||
}
|
||||
|
||||
.menu-tag.tag-warning {
|
||||
background: rgba(210, 155, 155, 0.2);
|
||||
color: #933;
|
||||
border-color: rgba(210, 155, 155, 0.4);
|
||||
}
|
||||
|
||||
.menu-card-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--schwarz);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.15s ease;
|
||||
box-shadow: 2px 2px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.menu-card-btn:active {
|
||||
transform: translate(2px, 2px);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-green {
|
||||
background: var(--tannengruen);
|
||||
color: var(--creme);
|
||||
}
|
||||
.btn-green:hover {
|
||||
background: #27624c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-rose {
|
||||
background: var(--altrosa);
|
||||
color: #222;
|
||||
}
|
||||
.btn-rose:hover {
|
||||
background: #daa8a8;
|
||||
}
|
||||
|
||||
.btn-yellow {
|
||||
background: #FFE885;
|
||||
color: #4a3e14;
|
||||
}
|
||||
.btn-yellow:hover {
|
||||
background: #ffeb99;
|
||||
}
|
||||
|
||||
.btn-creme {
|
||||
background: var(--creme);
|
||||
color: var(--schwarz);
|
||||
}
|
||||
.btn-creme:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Schnelle Buch-Statistik am Fuß */
|
||||
.home-footer-stats {
|
||||
margin-top: 28px;
|
||||
padding-top: 20px;
|
||||
border-top: 1.5px dashed rgba(30, 77, 59, 0.3);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--tannengruen);
|
||||
}
|
||||
|
||||
.stat-lbl {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.menu-spread {
|
||||
padding: 20px 14px;
|
||||
}
|
||||
.menu-board-heading {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
.home-header {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
.home-brand-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="home-body">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="home-header">
|
||||
<a href="/" class="home-brand">
|
||||
<span class="home-brand-icon">📖</span>
|
||||
<div>
|
||||
<h1 class="home-brand-title">What To Cook?</h1>
|
||||
<p class="home-brand-sub">Privates Kochbuch & Menüplaner</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="home-header-nav">
|
||||
<a href="/shopping-list" class="home-header-badge" title="Einkaufsliste aufrufen">
|
||||
<span>🛒</span>
|
||||
<span>{% if open_shopping_count > 0 %}{{ open_shopping_count }} offen{% else %}Einkaufsliste{% endif %}</span>
|
||||
</a>
|
||||
<a href="/cookbook" class="home-header-badge" title="Direkt ins Kochbuch">
|
||||
<span>📖</span>
|
||||
<span>{{ recipe_count }} Rezepte</span>
|
||||
</a>
|
||||
<a href="/logout" class="home-logout-btn" title="Abmelden">Abmelden</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hauptbereich: Das ausgeklappte Menü -->
|
||||
<main class="home-container">
|
||||
|
||||
<div class="menu-board-intro">
|
||||
<div class="menu-board-tagline">✦ Willkommen in deiner Küche ✦</div>
|
||||
<h2 class="menu-board-heading">Was kochen wir heute?</h2>
|
||||
<p class="menu-board-desc">
|
||||
Wähle einen Bereich aus der Menükarte – vom schnellen Zufallsgenerator bis hin zum vollständigen Kochbuch und deiner Einkaufsliste.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="menu-spread">
|
||||
<div class="menu-grid">
|
||||
|
||||
<!-- 1. ZUFALLSGENERATOR -->
|
||||
<div class="menu-card featured">
|
||||
<div class="menu-card-top">
|
||||
<div class="menu-card-icon-wrap" style="background: #FFE885;">🎲</div>
|
||||
<h3 class="menu-card-title">Zufallsgenerator</h3>
|
||||
<p class="menu-card-desc">
|
||||
Keine Idee was gekocht werden soll? Bis zu 5 Gerichte nach Aufwand auslosen und direkt in deinen Wochenplan übernehmen.
|
||||
</p>
|
||||
<div class="menu-card-tags">
|
||||
<span class="menu-tag">Hauptgerichte</span>
|
||||
<span class="menu-tag">Schnelle Auslosung</span>
|
||||
<span class="menu-tag tag-warning">Ohne Kuchen/Kekse</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/cookbook?open=generator" class="menu-card-btn btn-green">
|
||||
<span>🎲</span> <span>Generator starten</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 2. KOCHBUCH -->
|
||||
<div class="menu-card">
|
||||
<div class="menu-card-top">
|
||||
<div class="menu-card-icon-wrap" style="background: #E8F2EC;">📖</div>
|
||||
<h3 class="menu-card-title">Mein Kochbuch</h3>
|
||||
<p class="menu-card-desc">
|
||||
Blättere durch alle 11 Kapitel, finde deine Lieblingsgerichte und nutze die millimetergenaue DIN A4 Buchdruck-Engine.
|
||||
</p>
|
||||
<div class="menu-card-tags">
|
||||
<span class="menu-tag">{{ recipe_count }} Rezepte</span>
|
||||
<span class="menu-tag">11 Kapitel</span>
|
||||
<span class="menu-tag">A4 Druckausgabe</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/cookbook" class="menu-card-btn btn-creme">
|
||||
<span>📖</span> <span>Kochbuch aufschlagen</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 3. WOCHENMENÜPLAN -->
|
||||
<div class="menu-card">
|
||||
<div class="menu-card-top">
|
||||
<div class="menu-card-icon-wrap" style="background: #F8EFEA;">📅</div>
|
||||
<h3 class="menu-card-title">Wochenmenüplan</h3>
|
||||
<p class="menu-card-desc">
|
||||
Plane die Mahlzeiten der aktuellen Kalenderwoche (Früh, Mittag, Kaffee & Kuchen, Abend) mit schneller Rezept-Live-Suche.
|
||||
</p>
|
||||
<div class="menu-card-tags">
|
||||
<span class="menu-tag">Aktuell: KW {{ current_week }}</span>
|
||||
<span class="menu-tag">Früh / Mittag / Abend</span>
|
||||
<span class="menu-tag">Kaffee & Kuchen</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/cookbook?open=mealplan" class="menu-card-btn btn-rose">
|
||||
<span>📅</span> <span>Wochenplan ansehen</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 4. EINKAUFSLISTE -->
|
||||
<div class="menu-card">
|
||||
<div class="menu-card-top">
|
||||
<div class="menu-card-icon-wrap" style="background: #FFF9D2;">🛒</div>
|
||||
<h3 class="menu-card-title">Einkaufsliste</h3>
|
||||
<p class="menu-card-desc">
|
||||
Alle benötigten Zutaten aus dem Wochenplan per Klick übernehmen, eigene Notizen hinzufügen und bequem im Supermarkt abhaken.
|
||||
</p>
|
||||
<div class="menu-card-tags">
|
||||
<span class="menu-tag">{% if open_shopping_count > 0 %}{{ open_shopping_count }} offen{% else %}Keine offenen Posten{% endif %}</span>
|
||||
<span class="menu-tag">Zutatenübernahme</span>
|
||||
<span class="menu-tag">Mobil optimiert</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/shopping-list" class="menu-card-btn btn-yellow">
|
||||
<span>🛒</span> <span>Zur Einkaufsliste</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 5. REZEPT HINZUFÜGEN -->
|
||||
<div class="menu-card">
|
||||
<div class="menu-card-top">
|
||||
<div class="menu-card-icon-wrap" style="background: #F3EBE1;">➕</div>
|
||||
<h3 class="menu-card-title">Rezept erfassen</h3>
|
||||
<p class="menu-card-desc">
|
||||
Füge ein neues Rezept mit Zutatenliste, Zubereitungsschritten, Kochzeit und Portionsangaben zum Kochbuch hinzu.
|
||||
</p>
|
||||
<div class="menu-card-tags">
|
||||
<span class="menu-tag">Zutatenliste</span>
|
||||
<span class="menu-tag">Portionen</span>
|
||||
<span class="menu-tag">Foto-Upload</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/cookbook?open=new-recipe" class="menu-card-btn btn-creme">
|
||||
<span>➕</span> <span>Neues Rezept anlegen</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Fußzeile in der Menükarte -->
|
||||
<div class="home-footer-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-val">{{ recipe_count }}</span>
|
||||
<span class="stat-lbl">Gespeicherte Rezepte</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-val">{{ chapters|length }}</span>
|
||||
<span class="stat-lbl">Buch-Kapitel</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-val">KW {{ current_week }}</span>
|
||||
<span class="stat-lbl">Aktuelle Menüwoche</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-val">A4</span>
|
||||
<span class="stat-lbl">Druckfertig gebunden</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,639 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Einkaufsliste – What To Cook?</title>
|
||||
<link rel="stylesheet" href="/static/cookbook.css?v=4.5.4">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛒</text></svg>">
|
||||
<style>
|
||||
body.shopping-body {
|
||||
background-color: var(--grau-bg, #e6e2dc);
|
||||
background-image: radial-gradient(rgba(30, 77, 59, 0.06) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--schwarz);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Top Nav */
|
||||
.shop-nav {
|
||||
background-color: var(--creme);
|
||||
border-bottom: 2px solid var(--schwarz);
|
||||
padding: 10px 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.shop-nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.shop-nav-title {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: var(--tannengruen);
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.shop-nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shop-nav-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: var(--creme);
|
||||
border: 1.5px solid var(--schwarz);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--schwarz);
|
||||
text-decoration: none;
|
||||
box-shadow: 2px 2px 0px rgba(0,0,0,0.12);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.shop-nav-btn:hover {
|
||||
background: #fff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Hauptbereich Notizblock */
|
||||
.shop-main {
|
||||
max-width: 720px;
|
||||
width: 100%;
|
||||
margin: 28px auto 60px auto;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.notepad-card {
|
||||
background: #FFFDF7;
|
||||
border: 2.5px solid var(--schwarz);
|
||||
border-radius: 14px;
|
||||
box-shadow: 6px 8px 0px rgba(0,0,0,0.15);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 24px 22px;
|
||||
}
|
||||
|
||||
/* Rote Randlinie wie beim klassischen Notizblock */
|
||||
.notepad-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; bottom: 0; left: 44px;
|
||||
width: 2px;
|
||||
background-color: rgba(210, 155, 155, 0.4);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notepad-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 2px dashed rgba(30, 77, 59, 0.3);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.notepad-title-group h2 {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 2.3rem;
|
||||
color: var(--tannengruen);
|
||||
margin: 0 0 2px 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.notepad-subtitle {
|
||||
font-size: 0.88rem;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notepad-counter {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--tannengruen);
|
||||
background: #FFF9D2;
|
||||
border: 1.5px dashed #D4C165;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Schnelleingabe */
|
||||
.shop-add-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 22px;
|
||||
background: #FFF9D2;
|
||||
border: 2px solid var(--schwarz);
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
box-shadow: 3px 3px 0px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.shop-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1rem;
|
||||
padding: 8px 10px;
|
||||
outline: none;
|
||||
color: var(--schwarz);
|
||||
}
|
||||
.shop-input::placeholder {
|
||||
color: #8c8260;
|
||||
}
|
||||
|
||||
.shop-add-btn {
|
||||
background: var(--tannengruen);
|
||||
color: var(--creme);
|
||||
border: 1.5px solid var(--schwarz);
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.shop-add-btn:hover {
|
||||
background: #27624c;
|
||||
}
|
||||
|
||||
/* Aktionen Toolbar */
|
||||
.shop-actions-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px dotted rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.shop-action-btn {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(0,0,0,0.25);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
color: #555;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.shop-action-btn:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
color: #222;
|
||||
}
|
||||
.shop-action-btn.btn-wochenplan {
|
||||
background: rgba(30, 77, 59, 0.1);
|
||||
border-color: rgba(30, 77, 59, 0.3);
|
||||
color: var(--tannengruen);
|
||||
font-weight: 600;
|
||||
}
|
||||
.shop-action-btn.btn-wochenplan:hover {
|
||||
background: rgba(30, 77, 59, 0.18);
|
||||
}
|
||||
|
||||
/* Die Einkaufsliste */
|
||||
.shopping-items-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shopping-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 12px 8px 48px;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.06);
|
||||
transition: background 0.15s;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.shopping-item-row:hover {
|
||||
background: rgba(30, 77, 59, 0.03);
|
||||
}
|
||||
|
||||
.shopping-item-row.is-checked {
|
||||
opacity: 0.55;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.shopping-item-row.is-checked .item-text {
|
||||
text-decoration: line-through;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-checkbox {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid var(--schwarz);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--tannengruen);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-text {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 500;
|
||||
color: #222;
|
||||
line-height: 1.3;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.item-source-badge {
|
||||
font-size: 0.72rem;
|
||||
background: rgba(30, 77, 59, 0.08);
|
||||
color: var(--tannengruen);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-delete-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #aaa;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.item-delete-btn:hover {
|
||||
color: #b00;
|
||||
background: rgba(200, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Leerer Zustand */
|
||||
.shopping-empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-family: var(--font-hand);
|
||||
font-size: 1.7rem;
|
||||
color: var(--tannengruen);
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
max-width: 400px;
|
||||
margin: 0 auto 18px auto;
|
||||
}
|
||||
|
||||
/* Toast Notification */
|
||||
.shop-toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
background: var(--tannengruen);
|
||||
color: var(--creme);
|
||||
padding: 10px 20px;
|
||||
border-radius: 30px;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
|
||||
border: 1.5px solid var(--schwarz);
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
pointer-events: none;
|
||||
}
|
||||
.shop-toast.active {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="shopping-body">
|
||||
|
||||
<!-- Nav -->
|
||||
<header class="shop-nav">
|
||||
<a href="/" class="shop-nav-brand">
|
||||
<span style="font-size: 1.6rem;">🛒</span>
|
||||
<div>
|
||||
<h1 class="shop-nav-title">Einkaufsliste</h1>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="shop-nav-links">
|
||||
<a href="/" class="shop-nav-btn">
|
||||
<span>🏠</span> <span>Menü</span>
|
||||
</a>
|
||||
<a href="/cookbook" class="shop-nav-btn">
|
||||
<span>📖</span> <span>Kochbuch</span>
|
||||
</a>
|
||||
<a href="/cookbook?open=mealplan" class="shop-nav-btn">
|
||||
<span>📅</span> <span>Wochenplan</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Container -->
|
||||
<main class="shop-main">
|
||||
<div class="notepad-card">
|
||||
|
||||
<div class="notepad-header">
|
||||
<div class="notepad-title-group">
|
||||
<h2>Einkaufszettel</h2>
|
||||
<p class="notepad-subtitle">Woche {{ current_week }} • Zutaten & Besorgungen</p>
|
||||
</div>
|
||||
<div class="notepad-counter" id="shop-counter">
|
||||
<span id="checked-count">0</span> / <span id="total-count">{{ items|length }}</span> erledigt
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schnelleingabe -->
|
||||
<form class="shop-add-form" id="shop-add-form" onsubmit="onAddItemSubmit(event)">
|
||||
<input
|
||||
type="text"
|
||||
id="shop-add-input"
|
||||
class="shop-input"
|
||||
placeholder="✏️ Zutat oder Artikel notieren (z. B. 500g Mehl)..."
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
<button type="submit" class="shop-add-btn">
|
||||
<span>➕</span> <span>Hinzufügen</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Toolbar Aktionen -->
|
||||
<div class="shop-actions-bar">
|
||||
<div>
|
||||
<a href="/cookbook?open=mealplan&prompt_transfer=1" class="shop-action-btn btn-wochenplan" title="Zutaten aus dem aktuellen Wochenplan abrufen">
|
||||
<span>📅</span> <span>Aus Wochenplan übernehmen</span>
|
||||
</a>
|
||||
</div>
|
||||
<div style="display: flex; gap: 6px;">
|
||||
<button type="button" class="shop-action-btn" onclick="deleteCheckedItems()" title="Abgehakte Einträge löschen">
|
||||
<span>🧹</span> <span>Erledigte löschen</span>
|
||||
</button>
|
||||
<button type="button" class="shop-action-btn" onclick="clearAllItems()" title="Ganze Liste leeren">
|
||||
<span>🗑️</span> <span>Alles leeren</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items Liste -->
|
||||
<ul class="shopping-items-list" id="shopping-items-list">
|
||||
{% for item in items %}
|
||||
<li class="shopping-item-row {% if item.checked %}is-checked{% endif %}" id="shop-item-{{ item.id }}" data-id="{{ item.id }}">
|
||||
<label class="item-left">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="item-checkbox"
|
||||
{% if item.checked %}checked{% endif %}
|
||||
onchange="toggleItem({{ item.id }})"
|
||||
>
|
||||
<span class="item-text">
|
||||
{% if item.amount or item.unit %}
|
||||
<strong>{{ item.amount }} {{ item.unit }}</strong>
|
||||
{% endif %}
|
||||
{{ item.item_name }}
|
||||
{% if item.source %}
|
||||
<span class="item-source-badge">{{ item.source }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
<button type="button" class="item-delete-btn" onclick="deleteSingleItem({{ item.id }})" title="Löschen">×</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<!-- Leerer Zustand (wenn 0 Items) -->
|
||||
<div class="shopping-empty-state" id="shopping-empty-state" style="{% if items|length > 0 %}display: none;{% endif %}">
|
||||
<div class="empty-icon">🎉</div>
|
||||
<h3 class="empty-title">Alles eingekauft!</h3>
|
||||
<p class="empty-desc">Deine Einkaufsliste ist momentan leer. Füge oben neue Dinge hinzu oder übernimm Zutaten direkt aus deinem Wochenplan.</p>
|
||||
<a href="/cookbook?open=mealplan&prompt_transfer=1" class="shop-nav-btn" style="background: var(--tannengruen); color: var(--creme); font-weight: bold; padding: 10px 18px;">
|
||||
<span>📅</span> <span>Zutaten aus Wochenplan holen</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="shop-toast" class="shop-toast">✓ Gespeichert</div>
|
||||
|
||||
<script>
|
||||
function showToast(msg) {
|
||||
const toast = document.getElementById('shop-toast');
|
||||
toast.textContent = msg;
|
||||
toast.classList.add('active');
|
||||
setTimeout(() => toast.classList.remove('active'), 2500);
|
||||
}
|
||||
|
||||
function updateCounter() {
|
||||
const rows = document.querySelectorAll('.shopping-item-row');
|
||||
const total = rows.length;
|
||||
const checked = document.querySelectorAll('.shopping-item-row.is-checked').length;
|
||||
|
||||
document.getElementById('checked-count').textContent = checked;
|
||||
document.getElementById('total-count').textContent = total;
|
||||
|
||||
const emptyState = document.getElementById('shopping-empty-state');
|
||||
if (total === 0) {
|
||||
emptyState.style.display = 'block';
|
||||
} else {
|
||||
emptyState.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function onAddItemSubmit(e) {
|
||||
e.preventDefault();
|
||||
const input = document.getElementById('shop-add-input');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/shopping-list', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ item_name: text, source: 'Manuell' })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'ok' && data.item) {
|
||||
renderNewItem(data.item);
|
||||
input.value = '';
|
||||
showToast(`✓ "${text}" hinzugefügt`);
|
||||
updateCounter();
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Fehler beim Hinzufügen: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderNewItem(item) {
|
||||
const ul = document.getElementById('shopping-items-list');
|
||||
const li = document.createElement('li');
|
||||
li.className = `shopping-item-row ${item.checked ? 'is-checked' : ''}`;
|
||||
li.id = `shop-item-${item.id}`;
|
||||
li.setAttribute('data-id', item.id);
|
||||
|
||||
const amountStr = (item.amount || item.unit) ? `<strong>${escapeHtml(item.amount || '')} ${escapeHtml(item.unit || '')}</strong> ` : '';
|
||||
const sourceStr = item.source ? `<span class="item-source-badge">${escapeHtml(item.source)}</span>` : '';
|
||||
|
||||
li.innerHTML = `
|
||||
<label class="item-left">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="item-checkbox"
|
||||
${item.checked ? 'checked' : ''}
|
||||
onchange="toggleItem(${item.id})"
|
||||
>
|
||||
<span class="item-text">
|
||||
${amountStr}${escapeHtml(item.item_name)}${sourceStr}
|
||||
</span>
|
||||
</label>
|
||||
<button type="button" class="item-delete-btn" onclick="deleteSingleItem(${item.id})" title="Löschen">×</button>
|
||||
`;
|
||||
|
||||
// Vor den abgehakten Items einfügen
|
||||
const firstChecked = ul.querySelector('.shopping-item-row.is-checked');
|
||||
if (firstChecked) {
|
||||
ul.insertBefore(li, firstChecked);
|
||||
} else {
|
||||
ul.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleItem(id) {
|
||||
const row = document.getElementById(`shop-item-${id}`);
|
||||
if (!row) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/shopping-list/${id}/toggle`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (data.status === 'ok' && data.item) {
|
||||
if (data.item.checked) {
|
||||
row.classList.add('is-checked');
|
||||
// Nach unten schieben
|
||||
const ul = document.getElementById('shopping-items-list');
|
||||
ul.appendChild(row);
|
||||
} else {
|
||||
row.classList.remove('is-checked');
|
||||
}
|
||||
updateCounter();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSingleItem(id) {
|
||||
const row = document.getElementById(`shop-item-${id}`);
|
||||
if (row) row.remove();
|
||||
updateCounter();
|
||||
|
||||
try {
|
||||
await fetch(`/api/shopping-list/${id}`, { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCheckedItems() {
|
||||
const checkedRows = document.querySelectorAll('.shopping-item-row.is-checked');
|
||||
if (checkedRows.length === 0) {
|
||||
showToast('Keine erledigten Einträge vorhanden.');
|
||||
return;
|
||||
}
|
||||
checkedRows.forEach(r => r.remove());
|
||||
updateCounter();
|
||||
showToast('✓ Erledigte Einträge gelöscht');
|
||||
|
||||
try {
|
||||
await fetch('/api/shopping-list/checked', { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAllItems() {
|
||||
const total = document.querySelectorAll('.shopping-item-row').length;
|
||||
if (total === 0) return;
|
||||
if (!confirm('Möchtest du wirklich die gesamte Einkaufsliste leeren?')) return;
|
||||
|
||||
document.getElementById('shopping-items-list').innerHTML = '';
|
||||
updateCounter();
|
||||
showToast('✓ Einkaufsliste geleert');
|
||||
|
||||
try {
|
||||
await fetch('/api/shopping-list/all', { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', updateCounter);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user