Files
cookbook/app/templates/cookbook.html
T

2737 lines
116 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mein Kochbuch – What To Cook?</title>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1E4D3B">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="alternate icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/cookbook.css?v=4.6.5">
<style id="print-engine-styles">
/* Auf dem Bildschirm: Cover, Inhaltsverzeichnis und Seitenzahlen ausblenden */
@media screen {
.page.cover,
.page.toc-page,
.page-footer {
display: none !important;
}
}
/* ==========================================================================
A4 Buchdruck-Engine: Universell für Apple iOS (AirPrint), Android Chrome & Desktop
Jede Seite ist exakt 1 Blatt A4 (282mm). Schöne Ränder, echte Hintergründe.
========================================================================== */
@page {
size: A4 portrait;
margin: 0;
}
@media print {
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
color-adjust: exact !important;
box-sizing: border-box !important;
}
html, body.cookbook-body {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
overflow: visible !important;
background: #FAF6F0 !important;
background-color: #FAF6F0 !important;
color: var(--schwarz) !important;
display: block !important;
}
/* Alle Bildschirmelemente ausblenden */
.cb-nav,
.cb-filter-bar,
.no-print,
.desktop-nav-actions,
.mobile-nav-wrapper,
#search-empty-state,
.modal-backdrop,
.cb-toast,
.recipe-photo-empty,
.recipe-source-line,
.empty-chapter-section {
display: none !important;
visibility: hidden !important;
height: 0 !important;
min-height: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
.book-wrapper {
display: block !important;
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
overflow: visible !important;
}
/* Jede Seite ist ein eigenständiges DIN A4 Blatt mit 14mm Kopfabstand und 282mm Höhe */
.page {
display: block !important;
position: relative !important;
width: 210mm !important;
max-width: 210mm !important;
min-width: 210mm !important;
height: 282mm !important;
min-height: 282mm !important;
max-height: 282mm !important;
margin: 0 auto !important;
padding: 14mm 14mm 16mm 14mm !important;
box-sizing: border-box !important;
background: #FAF6F0 !important;
background-color: #FAF6F0 !important;
box-shadow: none !important;
border: none !important;
outline: none !important;
border-radius: 0 !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
overflow: hidden !important;
}
.page:first-child {
page-break-before: avoid !important;
break-before: avoid !important;
}
.page:last-child {
page-break-after: auto !important;
break-after: auto !important;
}
/* 1. COVER */
.page.cover {
display: block !important;
padding: 50mm 14mm 16mm 14mm !important;
text-align: center !important;
background-color: var(--creme, #FAF6F0) !important;
background-image: url("data:image/svg+xml,%3Csvg preserveAspectRatio='none' viewBox='0 0 200 800' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M 5 0 Q 15 200 0 400 T 10 800 L 20 800 Q 10 600 25 400 T 15 0 Z' fill='%231E4D3B' opacity='0.9'/%3E%3Cpath d='M 35 0 Q 25 250 40 500 T 30 800 L 55 800 Q 65 500 45 250 T 50 0 Z' fill='%231E4D3B' opacity='0.85'/%3E%3Cpath d='M 70 0 Q 80 300 65 600 T 75 800 L 85 800 Q 75 500 90 300 T 80 0 Z' fill='%231E4D3B' opacity='0.9'/%3E%3Cpath d='M 105 0 Q 95 200 110 400 T 100 800 L 125 800 Q 135 600 120 400 T 125 0 Z' fill='%231E4D3B' opacity='0.8'/%3E%3Cpath d='M 140 0 Q 150 250 135 500 T 145 800 L 155 800 Q 145 500 160 250 T 150 0 Z' fill='%231E4D3B' opacity='0.9'/%3E%3Cpath d='M 175 0 Q 165 300 180 600 T 170 800 L 195 800 Q 205 500 185 300 T 190 0 Z' fill='%231E4D3B' opacity='0.85'/%3E%3C/svg%3E") !important;
background-size: 100% 100% !important;
background-repeat: no-repeat !important;
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
}
.cover-box {
display: inline-block !important;
padding: 24mm 20mm !important;
max-width: 82% !important;
border: 3.5px solid var(--schwarz) !important;
background-color: var(--creme) !important;
transform: rotate(-1deg) !important;
box-sizing: border-box !important;
text-align: center !important;
margin: 0 auto !important;
}
.cover-box h1 {
font-family: var(--font-hand) !important;
font-size: 3.4rem !important;
line-height: 1.15 !important;
margin: 0 !important;
color: var(--schwarz) !important;
}
.cover-box p {
font-family: var(--font-hand) !important;
font-size: 1.6rem !important;
color: var(--tannengruen) !important;
margin-top: 12px !important;
margin-bottom: 0 !important;
}
/* 2. INHALTSVERZEICHNIS (TOC) */
.page.toc-page {
display: block !important;
padding: 14mm 14mm 16mm 14mm !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
}
.toc-header {
margin-bottom: 5mm !important;
}
.toc-columns {
display: block !important;
column-count: 2 !important;
column-gap: 8mm !important;
margin-bottom: 4mm !important;
}
.toc-chapter-group {
break-inside: avoid !important;
page-break-inside: avoid !important;
margin-bottom: 3.5mm !important;
}
.toc-recipe-item {
margin-bottom: 2px !important;
font-size: 0.82rem !important;
line-height: 1.22 !important;
}
/* 3. KAPITEL-TRENNSEITEN */
.divider {
display: block !important;
text-align: center !important;
padding: 38mm 14mm 16mm 14mm !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
}
.divider .chapter-number {
font-family: var(--font-hand) !important;
font-size: 2.8rem !important;
color: var(--altrosa) !important;
margin-bottom: -4px !important;
}
.divider h2 {
font-family: var(--font-hand) !important;
font-size: 3.4rem !important;
color: var(--tannengruen) !important;
margin: 0 0 16px 0 !important;
line-height: 1.15 !important;
}
.doodle-placeholder {
width: 110mm !important;
height: 110mm !important;
max-width: 110mm !important;
max-height: 110mm !important;
margin: 6mm auto 15mm auto !important;
border: none !important;
background: transparent !important;
display: block !important;
}
.doodle-placeholder img {
width: 100% !important;
height: 100% !important;
max-width: 110mm !important;
max-height: 110mm !important;
object-fit: contain !important;
margin: 0 auto !important;
}
/* 4. REZEPTSEITE */
.recipe-page {
display: block !important;
padding: 14mm 14mm 16mm 14mm !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
}
.recipe-header {
border-bottom: 1.5px dashed rgba(30, 77, 59, 0.35) !important;
padding-bottom: 2mm !important;
margin-bottom: 3mm !important;
}
.recipe-title {
font-size: 1.6rem !important;
line-height: 1.15 !important;
margin: 0 0 1.5mm 0 !important;
color: var(--tannengruen) !important;
}
.recipe-meta-row {
display: flex !important;
flex-wrap: wrap !important;
gap: 2mm 5mm !important;
font-size: 0.80rem !important;
color: #4a453f !important;
}
/* 2-Spalten-Layout als stabile CSS-Tabelle (verhindert iOS-Flex/Grid-Bugs) */
.recipe-grid {
display: table !important;
width: 100% !important;
table-layout: fixed !important;
border-collapse: collapse !important;
margin-bottom: 2mm !important;
}
.recipe-side {
display: table-cell !important;
width: 62mm !important;
vertical-align: top !important;
padding-right: 6mm !important;
}
.recipe-grid:has(.has-portion-matrix) .recipe-side {
width: 72mm !important;
}
.recipe-main {
display: table-cell !important;
vertical-align: top !important;
}
.recipe-photo-box {
width: 100% !important;
height: 32mm !important;
max-height: 32mm !important;
margin-bottom: 2.5mm !important;
}
.recipe-photo-box img {
width: 100% !important;
height: 32mm !important;
max-height: 32mm !important;
object-fit: cover !important;
border-radius: 6px !important;
border: 1px solid rgba(34, 34, 34, 0.2) !important;
}
.ingredients-box {
padding: 5px 8px !important;
background: rgba(255, 255, 255, 0.75) !important;
background-color: rgba(255, 255, 255, 0.75) !important;
border: 1.5px dashed rgba(30, 77, 59, 0.4) !important;
border-radius: 8px !important;
}
.section-hand-title {
font-size: 1.15rem !important;
margin: 0 0 3px 0 !important;
color: var(--tannengruen) !important;
border-bottom: 1.5px solid var(--altrosa) !important;
}
.ingredients-list {
display: flex !important;
flex-direction: column !important;
gap: 1.5px !important;
list-style: none !important;
padding: 0 !important;
margin: 0 !important;
}
.ingredients-list li {
font-size: 0.76rem !important;
line-height: 1.2 !important;
}
.ingredients-list li.ing-subheading {
font-size: 0.80rem !important;
font-weight: 700 !important;
margin-top: 2px !important;
padding-bottom: 1px !important;
}
.portion-matrix-table {
font-size: 0.70rem !important;
line-height: 1.18 !important;
width: 100% !important;
}
.portion-matrix-table th {
padding: 1px 2px !important;
font-size: 0.68rem !important;
}
.portion-matrix-table td {
padding: 1px 2px !important;
}
.instructions-steps {
display: flex !important;
flex-direction: column !important;
gap: 2.5px !important;
}
.step-card {
padding: 3px 6px !important;
margin-bottom: 2.5px !important;
font-size: 0.80rem !important;
line-height: 1.22 !important;
display: flex !important;
gap: 6px !important;
align-items: flex-start !important;
background: rgba(255, 255, 255, 0.6) !important;
background-color: rgba(255, 255, 255, 0.6) !important;
border-radius: 6px !important;
}
.step-badge {
width: 17px !important;
height: 17px !important;
font-size: 0.72rem !important;
flex-shrink: 0 !important;
background: #1E4D3B !important;
background-color: #1E4D3B !important;
color: #FAF6F0 !important;
border-radius: 50% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.step-text {
flex: 1 1 auto !important;
}
.notes-box,
.notes-box-empty {
display: block !important;
margin-top: 3.5mm !important;
padding: 6px 10px 8px 10px !important;
font-size: 0.80rem !important;
line-height: 1.25 !important;
background: #FFF9D2 !important;
background-color: #FFF9D2 !important;
border: 1.5px dashed #D4C165 !important;
border-radius: 6px !important;
min-height: 26mm !important;
box-sizing: border-box !important;
}
.notes-box-title {
font-weight: 700 !important;
font-size: 0.82rem !important;
margin-bottom: 3px !important;
color: #554400 !important;
}
.notes-blank-lines {
display: flex !important;
flex-direction: column !important;
gap: 0 !important;
margin-top: 2px !important;
}
.notes-blank-line {
border-bottom: 1.2px dotted rgba(140, 120, 60, 0.5) !important;
height: 6.5mm !important;
width: 100% !important;
}
.print-only {
display: block !important;
}
/* 5. SEITENZAHL FEST IN DEN ECKEN DES BLATTES VERANKERT */
.page-footer {
position: absolute !important;
bottom: 3.5mm !important;
left: 14mm !important;
right: 14mm !important;
width: auto !important;
display: block !important;
border: none !important;
border-top: none !important;
margin: 0 !important;
padding: 0 !important;
z-index: 100 !important;
}
.page-footer.page-even {
text-align: left !important;
}
.page-footer.page-odd {
text-align: right !important;
}
.page-footer .page-number {
font-family: var(--font-hand), 'Caveat', cursive !important;
font-size: 1.25rem !important;
font-weight: 700 !important;
color: #1E4D3B !important;
display: inline-block !important;
}
.cover .page-footer {
display: none !important;
}
}
</style>
</head>
<body class="cookbook-body">
<!-- Navigation Bar -->
<header class="cb-nav">
<a href="/cookbook" class="cb-nav-brand">
<span class="cb-brand-icon">📖</span>
<h1 class="cb-nav-title">Kochbuch</h1>
</a>
<!-- Rezept-Titel Live-Suche -->
<div class="cb-search-box no-print">
<span class="cb-search-icon">🔍</span>
<input type="search" id="cb-recipe-search" class="cb-search-input" placeholder="Rezept suchen..." oninput="filterRecipesByTitle(this.value)" autocomplete="off">
<button type="button" id="cb-search-clear" class="cb-search-clear" onclick="clearRecipeSearch()" style="display: none;" title="Suche leeren">✕</button>
</div>
<!-- Desktop Buttons (auf Bildschirmen > 768px) -->
<div class="cb-nav-actions desktop-nav-actions">
<button type="button" onclick="openMealPlanModal(event)" class="cb-nav-btn" title="Wochenmenü-Plan öffnen">
<span>📅</span> <span>Wochenplan</span>
</button>
<button type="button" onclick="openGeneratorModal(event)" class="cb-nav-btn" title="Essens-Zufallsgenerator öffnen">
<span>🎲</span> <span>Shuffle</span>
</button>
<button type="button" onclick="openRecipeModal(event)" class="cb-nav-btn" title="Neues Rezept eintragen">
<span>➕</span> <span>Rezept</span>
</button>
<button type="button" onclick="openPrintModal(event)" class="cb-nav-btn" title="Druckansicht & PDF">
<span>🖨️</span> <span>Drucken</span>
</button>
<a href="/" class="cb-nav-btn" title="Zur Startseite">
<span>🏠</span> <span>Startseite</span>
</a>
<a href="/shopping-list" class="cb-nav-btn" title="Einkaufsliste öffnen">
<span>🛒</span> <span>Einkaufsliste</span>
</a>
<a href="/logout" class="cb-nav-btn cb-nav-btn-logout" title="Abmelden">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
<!-- Mobile 3-Punkte (Kebab) Menü (auf Bildschirmen <= 768px) -->
<div class="mobile-nav-wrapper">
<button type="button" class="cb-kebab-btn" onclick="toggleMobileMenu(event)" aria-label="Menü öffnen" title="Menü">
<span>⋮</span>
</button>
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
<button type="button" onclick="handleMobileMenuAction(openMealPlanModal)" class="cb-mobile-item">
<span>📅</span> <span>Wochenmenü-Plan</span>
</button>
<button type="button" onclick="handleMobileMenuAction(openGeneratorModal)" class="cb-mobile-item">
<span>🎲</span> <span>Shuffle-Generator</span>
</button>
<button type="button" onclick="handleMobileMenuAction(openRecipeModal)" class="cb-mobile-item">
<span>➕</span> <span>Neues Rezept</span>
</button>
<button type="button" onclick="handleMobileMenuAction(openPrintModal)" class="cb-mobile-item">
<span>🖨️</span> <span>Drucken & PDF</span>
</button>
<hr class="cb-mobile-divider">
<a href="/" class="cb-mobile-item">
<span>🏠</span> <span>Startseite</span>
</a>
<a href="/shopping-list" class="cb-mobile-item">
<span>🛒</span> <span>Einkaufsliste</span>
</a>
<hr class="cb-mobile-divider">
<a href="/logout" class="cb-mobile-item cb-mobile-logout">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
</div>
</header>
<!-- Kapitel-Filter-Leiste (Schlank & horizontal scrollbar) -->
<nav class="cb-filter-bar no-print">
<div class="cb-filter-scroll-container">
<a href="/cookbook" class="cb-chip {% if current_chapter is none %}active{% endif %}">
Alle Kapitel
</a>
{% for chap in chapters %}
<a href="/cookbook?chapter={{ chap.number }}" class="cb-chip {% if current_chapter == chap.number %}active{% endif %}">
{{ chap.number }}. {{ chap.title }}
</a>
{% endfor %}
</div>
</nav>
<!-- Virtual Book Container -->
<main class="book-wrapper">
<!-- Leerer Suchergebnis-Zustand -->
<div id="search-empty-state" class="no-print" style="display: none;">
<div style="font-size: 2.2rem; margin-bottom: 8px;">🔍</div>
<h3 style="font-family: var(--font-hand); font-size: 1.6rem; color: var(--tannengruen); margin: 0 0 6px 0;">Kein passendes Rezept gefunden</h3>
<p style="color: #666; font-size: 0.95rem; margin: 0 0 14px 0;">Es gibt leider kein Rezept mit dem Titel &bdquo;<span id="search-empty-term" style="font-weight: bold; color: var(--schwarz);"></span>&ldquo;.</p>
<button type="button" onclick="clearRecipeSearch()" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme); font-size: 0.9rem;">
Alle Rezepte anzeigen
</button>
</div>
<!-- SEITE 1: COVER -->
{% if current_chapter is none or current_chapter == 1 %}
<div class="page cover" id="page-cover">
<div class="cover-box">
<h1>Meine<br>Lieblings-<br>rezepte</h1>
<p>Klassiker & Entdeckungen</p>
</div>
</div>
<!-- INHALTSVERZEICHNIS (TOC) -->
{% if total_recipes > 0 %}
<!-- TOC SEITE 1 (Seite 2 des Buchs) -->
<div class="page toc-page" id="page-toc-1">
<header class="toc-header">
<div class="toc-subhead">Übersicht aller Gerichte</div>
<h2 class="toc-title">Inhaltsverzeichnis{% if toc_page_count > 1 %} <span style="font-size: 1.3rem; font-weight: normal; color: var(--gold);">(Teil 1)</span>{% endif %}</h2>
<div class="toc-divider-ornament">✦ ✦ ✦</div>
</header>
<div class="toc-columns">
{% for chap in toc_p1 %}
<div class="toc-chapter-group" data-chapter="{{ chap.number }}">
<div class="toc-chapter-header">
<span class="toc-chap-icon">{{ chap.icon }}</span>
<span class="toc-chap-title">Kapitel {{ chap.number }}: {{ chap.title }}</span>
<span class="toc-dots"></span>
<span class="toc-page-num" data-page-ref="kapitel-{{ chap.number }}">{{ chap.page_number }}</span>
</div>
<ul class="toc-recipe-list">
{% for r in chap.recipes %}
<li class="toc-recipe-item" data-recipe-id="{{ r.id }}">
<a href="#rezept-{{ r.id }}" class="toc-recipe-link">
<span class="toc-rec-title">{{ r.title }}</span>
<span class="toc-dots"></span>
<span class="toc-page-num" data-page-ref="rezept-{{ r.id }}">{{ r.page_number }}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
<footer class="page-footer page-even">
<span class="page-number">2</span>
</footer>
</div>
<!-- TOC SEITE 2 (Seite 3 des Buchs, falls > 55 Rezepte) -->
{% if toc_page_count > 1 %}
<div class="page toc-page" id="page-toc-2">
<header class="toc-header">
<div class="toc-subhead">Übersicht aller Gerichte</div>
<h2 class="toc-title">Inhaltsverzeichnis <span style="font-size: 1.3rem; font-weight: normal; color: var(--gold);">(Teil 2)</span></h2>
<div class="toc-divider-ornament">✦ ✦ ✦</div>
</header>
<div class="toc-columns">
{% for chap in toc_p2 %}
<div class="toc-chapter-group" data-chapter="{{ chap.number }}">
<div class="toc-chapter-header">
<span class="toc-chap-icon">{{ chap.icon }}</span>
<span class="toc-chap-title">Kapitel {{ chap.number }}: {{ chap.title }}</span>
<span class="toc-dots"></span>
<span class="toc-page-num" data-page-ref="kapitel-{{ chap.number }}">{{ chap.page_number }}</span>
</div>
<ul class="toc-recipe-list">
{% for r in chap.recipes %}
<li class="toc-recipe-item" data-recipe-id="{{ r.id }}">
<a href="#rezept-{{ r.id }}" class="toc-recipe-link">
<span class="toc-rec-title">{{ r.title }}</span>
<span class="toc-dots"></span>
<span class="toc-page-num" data-page-ref="rezept-{{ r.id }}">{{ r.page_number }}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
<footer class="page-footer page-odd">
<span class="page-number">3</span>
</footer>
</div>
{% endif %}
{% endif %}
{% endif %}
<!-- KAPITEL & REZEPTE -->
{% for chap in chapters_to_display %}
{% set chap_recipes = recipes_by_chapter.get(chap.number, []) %}
{% if chap_recipes|length > 0 %}
<!-- KAPITEL-TRENNSEITE -->
<div class="page divider" id="kapitel-{{ chap.number }}">
<div class="chapter-number">Kapitel {{ chap.number }}</div>
<h2>{{ chap.title }}</h2>
<div class="doodle-placeholder {% if chap.illustration_url %}has-illustration{% endif %}">
{% if chap.illustration_url %}
<img src="{{ chap.illustration_url }}" alt="{{ chap.title }} Illustration" loading="lazy" decoding="async">
{% else %}
<span class="placeholder-text">[ {{ chap.doodle_hint }} ]</span>
{% endif %}
</div>
<footer class="page-footer {% if chap.page_number % 2 == 0 %}page-even{% else %}page-odd{% endif %}">
<span class="page-number" id="footer-page-kapitel-{{ chap.number }}">{{ chap.page_number }}</span>
</footer>
</div>
<!-- REZEPTE IN DIESEM KAPITEL -->
{% for recipe in chap_recipes %}
<article class="page recipe-page" id="rezept-{{ recipe.id }}">
<!-- Header (Ohne Kapitelangabe & ohne Quelle im Titel) -->
<header class="recipe-header">
<h3 class="recipe-title">{{ recipe.title }}</h3>
<div class="recipe-meta-row">
{% if recipe.effort %}
<div class="recipe-meta-item">
<span>⏱️</span>
<span>Aufwand:
{% if recipe.effort == 'gering' %}Gering 🟢{% elif recipe.effort == 'hoch' %}Hoch 🔴{% else %}Mittel 🟡{% endif %}
</span>
</div>
{% endif %}
{% if recipe.prep_time %}
<div class="recipe-meta-item">
<span>🔪</span>
<span>Zubereitung: {{ recipe.prep_time }}</span>
</div>
{% endif %}
{% if recipe.cook_time %}
<div class="recipe-meta-item">
<span>🍳</span>
<span>Kochen/Backen: {{ recipe.cook_time }}</span>
</div>
{% endif %}
{% if recipe.portions %}
<div class="recipe-meta-item">
<span>👥</span>
<span>{{ recipe.portions }} Portionen</span>
</div>
{% endif %}
{% if recipe.source and recipe.source.strip() %}
<div class="recipe-meta-item">
<span>📖</span>
<span>Quelle: {{ recipe.source.strip() }}</span>
</div>
{% endif %}
</div>
</header>
<!-- 2-Spalten Layout -->
<div class="recipe-grid">
<!-- Linke Spalte: Foto + Zutaten -->
<aside class="recipe-side">
<div class="recipe-photo-box">
{% if recipe.image_url %}
<img src="{{ recipe.image_url }}" alt="{{ recipe.title }}" loading="lazy" decoding="async">
{% else %}
<div class="recipe-photo-empty">
<span>📸</span>
<span>Kein Foto hinterlegt</span>
</div>
{% endif %}
</div>
<!-- Zutatenleiste ohne Portionsangabe (da schon in Kopfzeile) -->
<div class="ingredients-box {% if recipe.has_portion_table %}has-portion-matrix{% endif %}">
<h4 class="section-hand-title">Zutaten</h4>
{% if recipe.has_portion_table and recipe.portion_matrix %}
<!-- Tabelle für 2/4/6/8 Portionen (z. B. Waffeln / Pancakes) -->
<table class="portion-matrix-table">
<thead>
<tr>
<th class="matrix-th-ing">Zutat</th>
<th class="matrix-th-p">2 P.</th>
<th class="matrix-th-p">4 P.</th>
<th class="matrix-th-p">6 P.</th>
<th class="matrix-th-p">8 P.</th>
</tr>
</thead>
<tbody>
{% for row in recipe.portion_matrix %}
{% if row.is_header %}
<tr class="matrix-header-row">
<th colspan="5">{{ row.text }}</th>
</tr>
{% else %}
<tr class="matrix-row">
<td class="matrix-td-name">{{ row.name }}</td>
<td class="matrix-td-amt">{{ row.amounts[2] }}</td>
<td class="matrix-td-amt highlight">{{ row.amounts[4] }}</td>
<td class="matrix-td-amt">{{ row.amounts[6] }}</td>
<td class="matrix-td-amt">{{ row.amounts[8] }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% else %}
<!-- Normale kompakte Zutatenliste (kein Abschneiden im Druck) -->
<ul class="ingredients-list">
{% for line in recipe.ingredients.split('\n') %}
{% if line.strip() %}
{% set raw_line = line.strip() %}
{% if raw_line.endswith(':') or raw_line.lower().startswith('für ') %}
<li class="ing-subheading">{{ raw_line }}</li>
{% else %}
<li>{{ raw_line }}</li>
{% endif %}
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div>
</aside>
<!-- Rechte Spalte: Zubereitung + Notizen + Quelle -->
<section class="recipe-main">
<div>
<h4 class="section-hand-title">Zubereitung</h4>
<div class="instructions-steps">
{% for step in recipe.instructions.split('\n') %}
{% if step.strip() %}
<div class="step-card">
<span class="step-badge">{{ loop.index }}</span>
<div class="step-text">{{ step.strip() }}</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% if recipe.notes and recipe.notes.strip() %}
<div class="notes-box">
<div class="notes-box-title">💡 Tipp & Notiz</div>
<div>{{ recipe.notes.strip() }}</div>
</div>
{% else %}
<div class="notes-box notes-box-empty print-only">
<div class="notes-box-title">💡 Tipp & Notiz</div>
<div class="notes-blank-lines">
<div class="notes-blank-line"></div>
<div class="notes-blank-line"></div>
<div class="notes-blank-line"></div>
</div>
</div>
{% endif %}
<!-- Aktionen (nur digital sichtbar, nicht im Druck) -->
<div class="no-print" style="margin-top: auto; padding-top: 15px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1.5px dashed rgba(34,34,34,0.15); flex-wrap: wrap;">
<a href="/cookbook/recipe/{{ recipe.id }}" class="cb-btn cb-btn-outline" style="color: var(--tannengruen); border-color: var(--tannengruen); font-size: 0.9rem;">
🔗 Einzelansicht
</a>
<a href="/cookbook/edit/{{ recipe.id }}" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme); font-size: 0.9rem;">
✏️ Bearbeiten
</a>
<button onclick="deleteRecipe({{ recipe.id }}, '{{ recipe.title }}')" class="cb-btn cb-btn-outline" style="color: #c53030; border-color: #c53030; font-size: 0.9rem;">
🗑️ Löschen
</button>
</div>
</section>
</div>
<!-- Seitenzahl im Buch: Alternierend links (gerade) / rechts (ungerade) -->
<footer class="page-footer {% if recipe.page_number % 2 == 0 %}page-even{% else %}page-odd{% endif %}">
<span class="page-number" id="footer-page-rezept-{{ recipe.id }}">{{ recipe.page_number }}</span>
</footer>
</article>
{% endfor %}
{% else %}
<!-- LEERES KAPITEL: Nur im Web-Browser sichtbar -->
<div class="empty-chapter-section no-print" id="kapitel-{{ chap.number }}">
{% if current_chapter == chap.number %}
<div class="empty-chapter-header">
<div class="chapter-number">Kapitel {{ chap.number }}</div>
<h2>{{ chap.title }}</h2>
<div class="doodle-placeholder {% if chap.illustration_url %}has-illustration{% endif %}" style="margin: 15px auto;">
{% if chap.illustration_url %}
<img src="{{ chap.illustration_url }}" alt="{{ chap.title }} Illustration" loading="lazy" decoding="async">
{% else %}
<span class="placeholder-text">[ {{ chap.doodle_hint }} ]</span>
{% endif %}
</div>
</div>
{% endif %}
<div class="empty-chapter-box">
<p class="empty-chapter-text">
Noch keine Rezepte in Kapitel {{ chap.number }} ({{ chap.title }}).
</p>
<button type="button" onclick="openRecipeModal(event, {{ chap.number }})" class="cb-btn cb-btn-green">
➕ Erstes Rezept für dieses Kapitel anlegen
</button>
</div>
</div>
{% endif %}
{% endfor %}
</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">
<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">&times;</button>
</div>
<div class="cb-modal-body">
<!-- Week Navigation Toolbar -->
<div class="mealplan-week-bar">
<div class="mealplan-week-nav">
<button type="button" class="cb-btn cb-btn-outline" style="color: var(--tannengruen); border-color: var(--tannengruen); padding: 5px 12px; font-size: 0.9rem;" onclick="navigateMealPlanWeek(-1)">
◀ Vorige Woche
</button>
<button type="button" id="mp-btn-today" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme); padding: 5px 12px; font-size: 0.9rem;" onclick="loadCurrentWeekMealPlan()">
Heute
</button>
<button type="button" class="cb-btn cb-btn-outline" style="color: var(--tannengruen); border-color: var(--tannengruen); padding: 5px 12px; font-size: 0.9rem;" onclick="navigateMealPlanWeek(1)">
Nächste Woche ▶
</button>
</div>
<div class="mealplan-week-badge">
<span id="mp-week-title">KW 42</span>
<span id="mp-week-range" style="font-size: 0.95rem; font-weight: normal; color: #555;">(12.10. – 18.10.2026)</span>
<span id="mp-today-pill" class="mealplan-today-tag" style="display: none;">Aktuelle Woche</span>
</div>
<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>
</div>
</div>
<!-- 7-Tage-Grid -->
<div id="mealplan-days-container" class="mealplan-grid">
<!-- Dynamisch über JS geladen -->
</div>
</div>
</div>
</div>
<!-- Popover / Modal für Slot-Bearbeitung -->
<div id="modal-slot-editor" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-slot-editor')" style="z-index: 2200;">
<div class="cb-modal-card" style="max-width: 480px;">
<div class="cb-modal-header">
<h4 class="cb-modal-title" id="slot-editor-title">🍽️ Mahlzeit planen</h4>
<button type="button" class="cb-modal-close" onclick="closeSlotEditor()">&times;</button>
</div>
<div class="cb-modal-body">
<input type="hidden" id="se-day">
<input type="hidden" id="se-type" value="mittag">
<!-- Mahlzeittyp-Auswahl: Früh, Mittag, Kaffee, Abend -->
<div style="margin-bottom: 14px;">
<label class="cb-label">Mahlzeit:</label>
<div class="slot-type-chips">
<button type="button" class="slot-type-chip" data-type="frueh" onclick="selectSlotMealType('frueh')">🌅 Früh</button>
<button type="button" class="slot-type-chip active" data-type="mittag" onclick="selectSlotMealType('mittag')">☀️ Mittag</button>
<button type="button" class="slot-type-chip" data-type="kaffee" onclick="selectSlotMealType('kaffee')">☕ Kaffee & Kuchen</button>
<button type="button" class="slot-type-chip" data-type="abend" onclick="selectSlotMealType('abend')">🌙 Abend</button>
</div>
</div>
<!-- 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" 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;">
<div style="display: flex; gap: 8px;">
<button type="button" onclick="rollRandomRecipeForSlot()" class="cb-btn cb-btn-outline" style="color: var(--tannengruen); border-color: var(--tannengruen); font-size: 0.88rem;" title="Ein Zufallsgericht aus dem Kochbuch wählen">
🎲 Zufall
</button>
<button type="button" id="se-btn-delete" onclick="deleteCurrentSlotFromEditor()" class="cb-btn cb-btn-outline" style="color: #c53030; border-color: #e53e3e; font-size: 0.88rem; display: none;" title="Diese Mahlzeit entfernen">
🗑️ Löschen
</button>
</div>
<div style="display: flex; gap: 8px;">
<button type="button" onclick="closeSlotEditor()" class="cb-btn cb-btn-modal-cancel">
Abbrechen
</button>
<button type="button" onclick="saveSlotEditor()" class="cb-btn cb-btn-modal-submit">
💾 Speichern
</button>
</div>
</div>
</div>
</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">&times;</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()">&times;</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)-->
<!-- ========================================== -->
<div id="modal-generator" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-generator')">
<div class="cb-modal-card" style="max-width: 680px;">
<div class="cb-modal-header">
<h3 class="cb-modal-title"><span>🎲</span> Essens-Zufallsgenerator</h3>
<button type="button" class="cb-modal-close" onclick="closeGeneratorModal()" title="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<div style="margin-bottom: 16px;">
<label class="cb-label" style="display: block; margin-bottom: 6px;">Wie viele Gerichte auslosen?</label>
<div class="gen-count-pills">
<button type="button" class="gen-count-btn" onclick="setGenSlotCount(1)">1 Gericht</button>
<button type="button" class="gen-count-btn active" onclick="setGenSlotCount(2)">2 Gerichte</button>
<button type="button" class="gen-count-btn" onclick="setGenSlotCount(3)">3 Gerichte</button>
<button type="button" class="gen-count-btn" onclick="setGenSlotCount(4)">4 Gerichte</button>
<button type="button" class="gen-count-btn" onclick="setGenSlotCount(5)">5 Gerichte</button>
</div>
</div>
<!-- Slot Configs -->
<div id="gen-slots-container"></div>
<div style="margin: 20px 0 16px 0;">
<button type="button" id="btn-gen-roll" class="cb-btn cb-btn-creme" onclick="rollAllDishes()"
style="width: 100%; justify-content: center; font-size: 1.25rem; padding: 12px; background: var(--tannengruen); color: var(--creme);">
<span>🎲</span> <span>Zufallsgenerator starten</span>
</button>
</div>
<!-- Results Display -->
<div id="gen-results-section" style="display: none; border-top: 2px dashed rgba(34,34,34,0.15); padding-top: 18px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px;">
<h4 style="font-family: var(--font-hand); font-size: 1.5rem; color: var(--tannengruen); margin: 0;">
✨ Ausgeloste Gerichte
</h4>
<button type="button" onclick="rollAllDishes()" class="cb-btn cb-btn-outline" style="color: var(--schwarz); border-color: rgba(34,34,34,0.3); font-size: 0.9rem;">
<span>🔄</span> Alle neu
</button>
</div>
<div id="gen-results-list"></div>
</div>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL 2: NEUES REZEPT EINTRAGEN (no-print) -->
<!-- ========================================== -->
<div id="modal-recipe" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-recipe')">
<div class="cb-modal-card">
<div class="cb-modal-header">
<h3 class="cb-modal-title"><span>➕</span> Neues Rezept eintragen</h3>
<button type="button" class="cb-modal-close" onclick="closeRecipeModal()" title="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<form id="modal-recipe-form" enctype="multipart/form-data">
<!-- Titel -->
<div class="cb-form-group">
<label for="m-title" class="cb-label">Name des Rezepts *</label>
<input type="text" id="m-title" name="title" class="cb-input" placeholder="z. B. Klassische Waffeln" required autocomplete="off">
</div>
<!-- Kapitel -->
<div class="cb-form-group">
<label for="m-chapter_number" class="cb-label">Kategorie / Kapitel *</label>
<select id="m-chapter_number" name="chapter_number" class="cb-select" required>
<option value="" disabled selected>Kapitel wählen...</option>
{% for chap in chapters %}
<option value="{{ chap.number }}">Kapitel {{ chap.number }}: {{ chap.title }}</option>
{% endfor %}
</select>
</div>
<!-- Aufwand & Portionen -->
<div class="cb-row">
<div class="cb-col">
<div class="cb-form-group">
<label for="m-effort" class="cb-label">Aufwand *</label>
<select id="m-effort" name="effort" class="cb-select" required>
<option value="gering">Gering 🟢 (≤ 20 Min)</option>
<option value="mittel" selected>Mittel 🟡 (~ 45 Min)</option>
<option value="hoch">Hoch 🔴 (> 60 Min)</option>
</select>
</div>
</div>
<div class="cb-col">
<div class="cb-form-group">
<label for="m-portions" class="cb-label">Standard-Portionen</label>
<input type="number" id="m-portions" name="portions" class="cb-input" value="4" min="1" max="50">
</div>
</div>
</div>
<!-- Option für 2/4/6/8 Portionen-Tabelle -->
<div class="cb-form-group" style="background: rgba(30, 77, 59, 0.05); border: 1.5px solid rgba(30, 77, 59, 0.2); border-radius: 8px; padding: 10px 14px;">
<label class="cb-checkbox-label" style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin: 0;">
<input type="checkbox" id="m-has_portion_table" name="has_portion_table" value="1" style="width: 18px; height: 18px; accent-color: var(--tannengruen);">
<div>
<span style="font-weight: 700; font-size: 0.95rem; color: var(--tannengruen); display: block;">
📊 Tabelle für 2 / 4 / 6 / 8 Portionen berechnen
</span>
<span style="font-size: 0.82rem; color: #666;">
Ideal für Waffeln, Pancakes oder Teige – berechnet die Zutaten automatisch für 2, 4, 6 und 8 Portionen im Druck vor.
</span>
</div>
</label>
</div>
<!-- Zeiten -->
<div class="cb-row">
<div class="cb-col">
<div class="cb-form-group">
<label for="m-prep_time" class="cb-label">Zubereitungszeit</label>
<input type="text" id="m-prep_time" name="prep_time" class="cb-input" placeholder="z. B. 15 Min">
</div>
</div>
<div class="cb-col">
<div class="cb-form-group">
<label for="m-cook_time" class="cb-label">Koch- / Backzeit</label>
<input type="text" id="m-cook_time" name="cook_time" class="cb-input" placeholder="z. B. 30 Min">
</div>
</div>
</div>
<!-- Foto-Upload -->
<div class="cb-form-group">
<label class="cb-label">Foto vom Gericht (optional)</label>
<div class="upload-dropzone" onclick="document.getElementById('m-image-upload').click()">
<input type="file" id="m-image-upload" name="image_file" accept="image/*" style="display: none;">
<div style="font-size: 2rem; margin-bottom: 6px;">📷</div>
<div style="font-family: var(--font-hand); font-size: 1.15rem; color: var(--tannengruen); font-weight: 700;">
Klicken zum Foto auswählen
</div>
<div style="font-size: 0.85rem; color: #666; margin-top: 4px;">
JPEG, PNG oder WebP (wird automatisch komprimiert & optimiert)
</div>
</div>
<div id="m-preview-box" class="preview-container">
<img id="m-image-preview" src="" alt="Vorschau">
</div>
</div>
<!-- Zutaten -->
<div class="cb-form-group">
<label for="m-ingredients" class="cb-label">Zutaten (eine Zutat pro Zeile, z. B. "500g Mehl", "1 Prise Salz") *</label>
<textarea id="m-ingredients" name="ingredients" class="cb-textarea" rows="5" required
placeholder="250 g Mehl&#10;125 g Butter&#10;3 Eier&#10;250 ml Milch&#10;1 Pck. Vanillezucker&#10;1 Prise Salz"></textarea>
</div>
<!-- Zubereitung -->
<div class="cb-form-group">
<label for="m-instructions" class="cb-label">Zubereitungsschritte (ein Schritt pro Zeile) *</label>
<textarea id="m-instructions" name="instructions" class="cb-textarea" rows="5" required
placeholder="Butter mit Zucker schaumig schlagen.&#10;Eier einzeln unterrühren.&#10;Mehl mit Backpulver und Milch abwechselnd hinzugeben.&#10;Waffeleisen vorheizen und die Waffeln goldbraun backen."></textarea>
</div>
<!-- Notizen -->
<div class="cb-form-group">
<label for="m-notes" class="cb-label">💡 Tipps, Notizen & Kniffe (optional)</label>
<textarea id="m-notes" name="notes" class="cb-textarea" rows="2"
placeholder="Tipp: Mit etwas Mineralwasser werden die Waffeln besonders fluffig."></textarea>
</div>
<!-- Credits / Quelle -->
<div class="cb-form-group">
<label for="m-source" class="cb-label">Quelle / Credits (optional)</label>
<input type="text" id="m-source" name="source" class="cb-input" placeholder="z. B. Familienrezept, Oma Anna, Chefkoch">
</div>
<!-- Error Banner -->
<div id="m-form-error" style="display: none; background: #fff0f0; border: 2px solid #e53e3e; color: #9b1c1c; padding: 10px 14px; border-radius: 8px; margin-top: 14px; font-weight: 600;"></div>
<!-- Submit Buttons -->
<div style="display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px;">
<button type="button" onclick="closeRecipeModal()" class="cb-btn cb-btn-modal-cancel">
Abbrechen
</button>
<button type="submit" id="m-btn-submit" class="cb-btn cb-btn-modal-submit">
<span>💾</span> <span>Rezept speichern</span>
</button>
</div>
</form>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL 3: DRUCKEN & BUCHOPTIONEN (no-print) -->
<!-- ========================================== -->
<div id="modal-print" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-print')">
<div class="cb-modal-card" style="max-width: 540px;">
<div class="cb-modal-header">
<h3 class="cb-modal-title"><span>🖨️</span> Druckansicht & Buch drucken</h3>
<button type="button" class="cb-modal-close" onclick="closePrintModal()" title="Schließen">&times;</button>
</div>
<div class="cb-modal-body">
<p style="margin: 0 0 18px 0; font-size: 0.95rem; color: #444; line-height: 1.45;">
Hier kannst du das gesamte Kochbuch als A4-Buch drucken oder als PDF speichern.
</p>
<div style="display: flex; flex-direction: column; gap: 14px;">
<!-- Aktion 1: Jetzt drucken -->
<button type="button" onclick="executePrint()" class="cb-btn cb-btn-creme"
style="justify-content: center; font-size: 1.15rem; padding: 14px; background: var(--tannengruen); color: var(--creme); width: 100%;">
<span>🖨️</span> <span>Jetzt Drucken / Als PDF speichern</span>
</button>
<!-- Aktion 2: Druckdatum im Hintergrund vermerken -->
<div style="border-top: 1.5px dashed rgba(34,34,34,0.15); padding-top: 16px; margin-top: 4px;">
<p style="font-size: 0.85rem; color: #666; margin: 0 0 10px 0; line-height: 1.35;">
<strong>Druckdatum in Datenbank festhalten:</strong> Wenn du das Buch binden lässt, kannst du das aktuelle Datum intern für alle Rezepte speichern.
</p>
<button type="button" onclick="bulkMarkCurrentPrinted()" class="cb-btn cb-btn-outline"
style="width: 100%; color: #166534; border-color: #22c55e; background: #f0fdf4; font-size: 0.9rem; padding: 9px 12px; justify-content: center;">
<span>📅</span> <span>Heutiges Druckdatum in Datenbank speichern</span>
</button>
</div>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 24px;">
<button type="button" onclick="closePrintModal()" class="cb-btn cb-btn-modal-cancel">
Schließen
</button>
</div>
</div>
</div>
</div>
<div id="cb-toast" class="cb-toast" style="display: none;"></div>
<script>
// ==========================================
// MOBILE NAVIGATION (3-PUNKTE-MENÜ)
// ==========================================
function toggleMobileMenu(e) {
if (e) e.stopPropagation();
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.toggle('is-open');
}
function handleMobileMenuAction(fn) {
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
if (fn) fn();
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.mobile-nav-wrapper')) {
const menu = document.getElementById('cb-mobile-menu');
if (menu) menu.classList.remove('is-open');
}
});
// ==========================================
// REZEPT-TITEL LIVE-SUCHE
// ==========================================
function filterRecipesByTitle(query) {
const q = (query || '').trim().toLowerCase();
const clearBtn = document.getElementById('cb-search-clear');
if (clearBtn) {
clearBtn.style.display = q ? 'block' : 'none';
}
const recipeCards = document.querySelectorAll('.recipe-page');
const dividers = document.querySelectorAll('.page.divider');
const emptyState = document.getElementById('search-empty-state');
if (!q) {
recipeCards.forEach(p => p.classList.remove('search-hidden'));
dividers.forEach(d => d.classList.remove('search-hidden'));
if (emptyState) emptyState.style.display = 'none';
return;
}
let matchedCount = 0;
const chaptersWithMatches = new Set();
recipeCards.forEach(card => {
const titleEl = card.querySelector('.recipe-title');
const titleText = titleEl ? titleEl.textContent.toLowerCase() : '';
const match = titleText.includes(q);
if (match) {
card.classList.remove('search-hidden');
matchedCount++;
let prev = card.previousElementSibling;
while (prev) {
if (prev.classList.contains('divider')) {
chaptersWithMatches.add(prev.id);
break;
}
prev = prev.previousElementSibling;
}
} else {
card.classList.add('search-hidden');
}
});
dividers.forEach(div => {
if (chaptersWithMatches.has(div.id)) {
div.classList.remove('search-hidden');
} else {
div.classList.add('search-hidden');
}
});
if (emptyState) {
if (matchedCount === 0) {
emptyState.style.display = 'block';
const termEl = document.getElementById('search-empty-term');
if (termEl) termEl.textContent = query;
} else {
emptyState.style.display = 'none';
}
}
}
function clearRecipeSearch() {
const searchInput = document.getElementById('cb-recipe-search');
if (searchInput) {
searchInput.value = '';
filterRecipesByTitle('');
searchInput.focus();
}
}
// ==========================================
// DRUCKMODAL
// ==========================================
function openPrintModal(e) {
if (e) e.preventDefault();
document.getElementById('modal-print').classList.add('active');
document.body.classList.add('modal-open');
}
function closePrintModal() {
document.getElementById('modal-print').classList.remove('active');
document.body.classList.remove('modal-open');
}
function executePrint() {
closePrintModal();
setTimeout(() => {
window.print();
}, 250);
}
async function bulkMarkCurrentPrinted() {
const recipeCards = document.querySelectorAll('article.recipe-page');
const recipeIds = Array.from(recipeCards).map(card => {
return parseInt(card.id.replace('rezept-', ''), 10);
}).filter(id => !isNaN(id));
if (recipeIds.length === 0) {
alert('Keine Rezepte vorhanden.');
return;
}
if (!confirm(`Möchtest du für ${recipeIds.length} Rezepte das heutige Druckdatum in der Datenbank vermerken?`)) {
return;
}
try {
const res = await fetch('/api/recipes/mark-printed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ recipe_ids: recipeIds })
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Fehler beim Markieren');
showToast(`✅ Druckdatum für ${data.marked_count || recipeIds.length} Rezepte gespeichert`);
closePrintModal();
} catch (err) {
alert(err.message);
}
}
// ==========================================
// REZEPT LÖSCHEN & TOAST
// ==========================================
async function deleteRecipe(id, title) {
if (!confirm(`Möchtest du das Rezept "${title}" wirklich löschen?`)) return;
try {
const res = await fetch(`/api/recipes/${id}`, { method: 'DELETE' });
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Fehler beim Löschen');
showToast('🗑️ Rezept gelöscht');
setTimeout(() => window.location.reload(), 600);
} catch (err) {
alert(err.message);
}
}
function showToast(msg) {
const t = document.getElementById('cb-toast');
t.textContent = msg;
t.style.display = 'flex';
setTimeout(() => { t.style.display = 'none'; }, 3500);
}
// ==========================================
// MODAL CONTROLLER & BACKDROP
// ==========================================
function openGeneratorModal(e) {
if (e) e.preventDefault();
document.getElementById('modal-generator').classList.add('active');
document.body.classList.add('modal-open');
initGeneratorSlots();
}
function closeGeneratorModal() {
document.getElementById('modal-generator').classList.remove('active');
document.body.classList.remove('modal-open');
}
function openRecipeModal(e, chapterNum, prefillTitle, dishCat) {
if (e) e.preventDefault();
if (document.getElementById('modal-generator').classList.contains('active')) {
closeGeneratorModal();
}
const select = document.getElementById('m-chapter_number');
if (chapterNum) {
select.value = chapterNum;
} else if (dishCat) {
const chap = GEN_CHAPTERS.find(c => c.title.toLowerCase().includes(dishCat.toLowerCase()) || dishCat.toLowerCase().includes(c.title.toLowerCase()));
if (chap) {
select.value = chap.number;
} else {
const catMap = {
'Suppen': 1, 'Eintöpfe': 1, 'Hausmannskost': 2, 'Nudeln': 3,
'Kartoffeln': 4, 'Reis': 5, 'Teigig': 6, 'asiatisch': 7,
'Süßspeisen': 8, 'Fisch': 2
};
if (catMap[dishCat]) select.value = catMap[dishCat];
}
}
if (prefillTitle) {
document.getElementById('m-title').value = prefillTitle;
}
document.getElementById('modal-recipe').classList.add('active');
document.body.classList.add('modal-open');
}
function closeRecipeModal() {
document.getElementById('modal-recipe').classList.remove('active');
document.body.classList.remove('modal-open');
}
function onBackdropClick(e, modalId) {
if (e.target.id === modalId) {
if (modalId === 'modal-mealplan') closeMealPlanModal();
if (modalId === 'modal-slot-editor') closeSlotEditor();
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();
closeRecipeModal();
closePrintModal();
}
});
// Touch-Event Isolation: Verhindert Hintergrund-Scrollen
document.querySelectorAll('.cb-modal-backdrop').forEach(modal => {
modal.addEventListener('touchmove', function(e) {
if (e.target === modal) {
e.preventDefault();
}
}, { passive: false });
});
// ==========================================
// SHUFFLE / GENERATOR LOGIK MIT DOODLE-ICONS
// ==========================================
const GEN_CHAPTERS = [
{% for chap in chapters %}
{
number: {{ chap.number }},
title: "{{ chap.title }}",
icon: "{{ chap.icon }}",
illustration: "{{ chap.illustration_url }}",
count: {{ recipe_counts.get(chap.number, 0) }}
},
{% endfor %}
];
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: [] },
{ category: "", efforts: [] },
{ category: "", efforts: [] },
{ category: "", efforts: [] },
{ category: "", efforts: [] }
];
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();
}
function renderGenSlots() {
const container = document.getElementById('gen-slots-container');
let html = '';
for (let i = 0; i < genSlotCount; i++) {
const slot = genSlots[i];
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');
const isHoch = slot.efforts && slot.efforts.includes('hoch');
html += `
<div class="gen-slot-item">
<div class="gen-slot-header">
<span>🍽️ Gericht ${i + 1}</span>
</div>
<div class="cb-row" style="gap: 12px; align-items: flex-start;">
<!-- 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}">
<button type="button" class="chapter-select-btn" onclick="toggleChapterDropdown(${i}, event)">
<div class="chapter-select-btn-content">
${selectedChap ? `
<img src="${selectedChap.illustration}" class="chapter-select-thumb" alt="${selectedChap.title}">
<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 Hauptgerichte (Beliebig)</span>
`}
</div>
<span class="chapter-select-arrow">▼</span>
</button>
<div class="chapter-dropdown-menu" id="chapter-dropdown-menu-${i}">
<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 Hauptgerichte / Beliebig</div>
<div style="font-size: 0.8rem; color: #666;">Zufall aus allen herzhaften Kategorien</div>
</div>
<span class="chapter-recipe-count">${SHUFFLE_TOTAL_RECIPES} Rezepte</span>
</div>
${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">
<div class="chapter-option-title">${c.title}</div>
</div>
<span class="chapter-recipe-count ${c.count === 0 ? 'zero' : ''}">${c.count} ${c.count === 1 ? 'Rezept' : 'Rezepte'}</span>
</div>
`).join('')}
</div>
</div>
</div>
<!-- Aufwand Filter (Mehrfachauswahl!) -->
<div class="cb-col" style="flex: 1.4; min-width: 170px;">
<label style="font-size: 0.85rem; color: #555; font-weight: 700; margin-bottom: 4px; display: block;">Aufwand (Mehrfachauswahl):</label>
<div class="gen-effort-chips">
<span class="gen-effort-chip ${isAllEfforts ? 'selected' : ''}" onclick="onGenEffortToggle(${i}, '')">Alle</span>
<span class="gen-effort-chip ${isGering ? 'selected' : ''}" onclick="onGenEffortToggle(${i}, 'gering')">🟢 Gering</span>
<span class="gen-effort-chip ${isMittel ? 'selected' : ''}" onclick="onGenEffortToggle(${i}, 'mittel')">🟡 Mittel</span>
<span class="gen-effort-chip ${isHoch ? 'selected' : ''}" onclick="onGenEffortToggle(${i}, 'hoch')">🔴 Hoch</span>
</div>
</div>
</div>
</div>
`;
}
container.innerHTML = html;
}
function toggleChapterDropdown(slotIdx, e) {
if (e) e.stopPropagation();
const current = document.getElementById(`custom-chapter-select-${slotIdx}`);
const wasOpen = current ? current.classList.contains('is-open') : false;
document.querySelectorAll('.custom-chapter-select').forEach(el => el.classList.remove('is-open'));
if (current && !wasOpen) {
current.classList.add('is-open');
}
}
function selectSlotChapter(slotIdx, chapNumber) {
genSlots[slotIdx].category = chapNumber ? String(chapNumber) : "";
renderGenSlots();
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.custom-chapter-select')) {
document.querySelectorAll('.custom-chapter-select').forEach(el => el.classList.remove('is-open'));
}
});
function onGenEffortToggle(slotIdx, val) {
if (!val) {
genSlots[slotIdx].efforts = [];
} else {
if (!genSlots[slotIdx].efforts) genSlots[slotIdx].efforts = [];
const idx = genSlots[slotIdx].efforts.indexOf(val);
if (idx >= 0) {
genSlots[slotIdx].efforts.splice(idx, 1);
} else {
genSlots[slotIdx].efforts.push(val);
}
if (genSlots[slotIdx].efforts.length === 3) {
genSlots[slotIdx].efforts = [];
}
}
renderGenSlots();
}
async function rollAllDishes() {
const btn = document.getElementById('btn-gen-roll');
btn.disabled = true;
btn.innerHTML = '<span>⏳</span> <span>Auslosen läuft...</span>';
const payload = { slots: [] };
for (let i = 0; i < genSlotCount; i++) {
const slot = genSlots[i];
const chapNum = slot.category ? parseInt(slot.category, 10) : null;
payload.slots.push({
slot_number: i + 1,
chapter_number: chapNum,
category: slot.category || null,
efforts: (slot.efforts && slot.efforts.length > 0) ? slot.efforts : ["gering", "mittel", "hoch"],
current_dish_id: genCurrentResults[i]?.recipe?.id || genCurrentResults[i]?.dish?.id || null
});
}
try {
const res = await fetch('/api/roll', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Fehler beim Auslosen');
genCurrentResults = data.results || [];
renderGenResults();
} catch (err) {
alert(err.message);
} finally {
btn.disabled = false;
btn.innerHTML = '<span>🎲</span> <span>Zufallsgenerator starten</span>';
}
}
async function rerollSingleDish(slotNumber) {
const idx = slotNumber - 1;
const slot = genSlots[idx];
const chapNum = slot.category ? parseInt(slot.category, 10) : null;
const otherIds = genCurrentResults
.filter((r, i) => i !== idx && (r.recipe || r.dish))
.map(r => (r.recipe ? r.recipe.id : r.dish.id));
const payload = {
slot_number: slotNumber,
chapter_number: chapNum,
category: slot.category || null,
efforts: (slot.efforts && slot.efforts.length > 0) ? slot.efforts : ["gering", "mittel", "hoch"],
current_dish_id: genCurrentResults[idx]?.recipe?.id || genCurrentResults[idx]?.dish?.id || null,
other_dish_ids: otherIds
};
try {
const res = await fetch('/api/roll-single', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.status === 'ok') {
genCurrentResults[idx] = data;
renderGenResults();
} else {
alert(data.message || 'Kein anderes Rezept gefunden');
}
} catch (err) {
alert(err.message);
}
}
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;
}
const effortBadge = (eff) => {
if (eff === 'gering') return '<span class="gen-tag" style="background:#e6f9f0; color:#1e7e34; border-color:#b2ecc7;">🟢 Gering (≤20m)</span>';
if (eff === 'hoch') return '<span class="gen-tag" style="background:#ffeef0; color:#c53030; border-color:#feb2b2;">🔴 Hoch (>60m)</span>';
return '<span class="gen-tag" style="background:#fffbeb; color:#b7791f; border-color:#fde68a;">🟡 Mittel (~45m)</span>';
};
list.innerHTML = genCurrentResults.map((r, i) => {
const recipeItem = r.recipe || r.dish;
if (r.status !== 'ok' || !recipeItem) {
const cNum = r.chapter_number;
return `
<div class="gen-result-card" style="border-color:#d9534f; background: #fffcfc;">
<div class="gen-dish-name" style="color:#c53030; font-size:1.3rem;">Gericht ${i + 1}: Kein Rezept gefunden</div>
<p style="margin:4px 0 8px 0; font-size:0.95rem; color:#666;">${r.message || 'Keine passenden Rezepte im Kochbuch vorhanden.'}</p>
${cNum ? `
<button type="button" onclick="openRecipeModal(event, ${cNum})" class="cb-btn cb-btn-modal-submit" style="font-size:0.95rem; padding: 6px 14px; align-self: flex-start;">
➕ Erstes Rezept für Kapitel ${cNum} anlegen
</button>
` : `
<button type="button" onclick="openRecipeModal(event)" class="cb-btn cb-btn-modal-submit" style="font-size:0.95rem; padding: 6px 14px; align-self: flex-start;">
➕ Neues Rezept anlegen
</button>
`}
</div>
`;
}
const d = recipeItem;
const chapTitle = d.chapter_title || d.category || 'Kochbuch';
const chapNum = d.chapter_number || '';
const chapImg = d.chapter_illustration || '';
return `
<div class="gen-result-card">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap: 8px;">
<div style="flex: 1; min-width: 0;">
<div class="gen-badge-row" style="margin-bottom:6px;">
<span class="gen-tag gen-tag-chapter">
${chapImg ? `<img src="${chapImg}" class="gen-chapter-thumb" alt="${chapTitle}">` : ''}
<span>${chapNum ? 'Kapitel ' + chapNum + ': ' : ''}${chapTitle}</span>
</span>
${effortBadge(d.effort)}
${d.cook_time ? `<span class="gen-tag" style="background:#f4f4f4; color:#444;">⏱️ ${d.cook_time}</span>` : ''}
</div>
<h5 class="gen-dish-name">${d.title || d.name}</h5>
</div>
<button type="button" onclick="rerollSingleDish(${i + 1})" class="cb-btn cb-btn-outline"
style="color:var(--schwarz); border-color:rgba(34,34,34,0.3); font-size:0.85rem; padding:5px 10px; flex-shrink: 0;" title="Nur dieses Gericht neu auslosen">
🔄 Neu
</button>
</div>
<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>
`;
}).join('');
}
function jumpToRecipe(recipeId, e) {
if (e) e.preventDefault();
sessionStorage.setItem('wtc_from_shuffle', 'true');
checkShuffleReturnState();
closeGeneratorModal();
const el = document.getElementById(`rezept-${recipeId}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
el.style.transition = 'outline 0.3s ease';
el.style.outline = '4px solid var(--altrosa)';
setTimeout(() => { el.style.outline = 'none'; }, 2000);
} else {
window.location.href = `/cookbook#rezept-${recipeId}`;
}
}
// ==========================================
// 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
// ==========================================
const mFileInput = document.getElementById('m-image-upload');
const mPreviewBox = document.getElementById('m-preview-box');
const mPreviewImg = document.getElementById('m-image-preview');
const mErrorBanner = document.getElementById('m-form-error');
let mPreparedImageFile = null;
mFileInput.addEventListener('change', async function() {
const file = this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
mPreviewImg.src = e.target.result;
mPreviewBox.style.display = 'block';
};
reader.readAsDataURL(file);
try {
mPreparedImageFile = await compressImageFile(file, 1600, 1600, 0.85);
} catch (err) {
mPreparedImageFile = file;
}
});
function compressImageFile(file, maxWidth, maxHeight, quality) {
return new Promise((resolve) => {
if (file.type === 'image/svg+xml' || file.type === 'image/gif') return resolve(file);
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
let w = img.width, h = img.height;
if (w > maxWidth || h > maxHeight) {
if (w > h) { h = Math.round((h * maxWidth) / w); w = maxWidth; }
else { w = Math.round((w * maxHeight) / h); h = maxHeight; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
canvas.toBlob((blob) => {
if (!blob) return resolve(file);
resolve(new File([blob], file.name.replace(/\.[^/.]+$/, "") + ".jpg", { type: "image/jpeg" }));
}, 'image/jpeg', quality);
};
img.onerror = () => { URL.revokeObjectURL(url); resolve(file); };
img.src = url;
});
}
const mForm = document.getElementById('modal-recipe-form');
const mBtnSubmit = document.getElementById('m-btn-submit');
mForm.addEventListener('submit', async function(e) {
e.preventDefault();
mErrorBanner.style.display = 'none';
mBtnSubmit.disabled = true;
mBtnSubmit.innerHTML = '<span>⏳</span> <span>Wird gespeichert...</span>';
const formData = new FormData(mForm);
if (mPreparedImageFile) {
formData.set('image_file', mPreparedImageFile, mPreparedImageFile.name);
}
try {
const res = await fetch('/api/recipes', {
method: 'POST',
body: formData
});
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Fehler beim Speichern');
closeRecipeModal();
showToast('🎉 Rezept erfolgreich angelegt!');
setTimeout(() => {
window.location.href = `/cookbook?chapter=${formData.get('chapter_number')}#rezept-${data.recipe.id}`;
window.location.reload();
}, 600);
} catch (err) {
mErrorBanner.textContent = err.message;
mErrorBanner.style.display = 'block';
mBtnSubmit.disabled = false;
mBtnSubmit.innerHTML = '<span>💾</span> <span>Rezept speichern</span>';
}
});
// ==========================================
// WOCHENMENÜ-PLAN CONTROLLER
// ==========================================
let mpCurrentYear = null;
let mpCurrentWeek = null;
let mpData = null;
function openMealPlanModal(e) {
if (e) e.preventDefault();
document.getElementById('modal-mealplan').classList.add('active');
document.body.classList.add('modal-open');
if (!mpCurrentYear || !mpCurrentWeek) {
loadMealPlan();
} else {
loadMealPlan(mpCurrentYear, mpCurrentWeek);
}
}
function closeMealPlanModal() {
document.getElementById('modal-mealplan').classList.remove('active');
document.body.classList.remove('modal-open');
}
function loadCurrentWeekMealPlan() {
loadMealPlan();
}
function navigateMealPlanWeek(direction) {
if (!mpCurrentYear || !mpCurrentWeek) return;
let y = mpCurrentYear;
let w = mpCurrentWeek + direction;
if (w < 1) {
y -= 1;
w = 52;
} else if (w > 52) {
y += 1;
w = 1;
}
loadMealPlan(y, w);
}
async function loadMealPlan(year, week) {
const container = document.getElementById('mealplan-days-container');
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #777; grid-column: 1 / -1;"><span>⏳</span> Wochenplan wird geladen...</div>';
let url = '/api/mealplan';
if (year && week) {
url += `?year=${year}&week=${week}`;
}
try {
const res = await fetch(url);
const data = await res.json();
if (!res.ok) throw new Error(data.detail || 'Fehler beim Laden des Wochenplans');
mpData = data;
mpCurrentYear = data.year;
mpCurrentWeek = data.week;
document.getElementById('mp-week-title').textContent = `KW ${data.week}`;
document.getElementById('mp-week-range').textContent = `(${data.range_formatted})`;
document.getElementById('mp-today-pill').style.display = data.is_current_week ? 'inline-block' : 'none';
document.getElementById('mp-btn-today').style.display = data.is_current_week ? 'none' : 'inline-flex';
renderMealPlanGrid(data);
} catch (err) {
container.innerHTML = `<div style="text-align: center; padding: 30px; color: #c53030; grid-column: 1 / -1;">❌ ${err.message}</div>`;
}
}
function renderMealPlanGrid(data) {
const container = document.getElementById('mealplan-days-container');
const mealTypes = [
{ key: 'frueh', label: '🌅 Früh' },
{ key: 'mittag', label: '☀️ Mittag' },
{ key: 'kaffee', label: '☕ Kaffee & Kuchen' },
{ key: 'abend', label: '🌙 Abend' }
];
let html = '';
data.days.forEach(day => {
const isTodayClass = day.is_today ? 'is-today' : '';
// Finde alle belegten Mahlzeiten an diesem Tag
const filledSlots = [];
mealTypes.forEach(mt => {
const slotKey = `${day.day_index}_${mt.key}`;
const slot = data.slots ? data.slots[slotKey] : null;
if (slot && (slot.recipe_title || (slot.custom_text && slot.custom_text.trim()))) {
filledSlots.push({ mt: mt, slot: slot });
}
});
html += `
<div class="mealplan-day-card ${isTodayClass}">
<div class="mealplan-day-header">
<span class="mealplan-day-name">${day.name}</span>
<span class="mealplan-day-date">${day.date_formatted}</span>
</div>
<div class="mealplan-day-slots">
`;
if (filledSlots.length > 0) {
filledSlots.forEach(item => {
const mt = item.mt;
const slot = item.slot;
html += `
<div class="mealplan-slot has-meal" id="slot-card-${day.day_index}_${mt.key}">
<div class="mealplan-slot-top">
<span class="mealplan-slot-badge">${mt.label}</span>
<div class="mealplan-slot-actions">
<button type="button" class="mealplan-action-btn" onclick="openSlotEditor(${day.day_index}, '${mt.key}', '${day.name}')" title="Mahlzeit bearbeiten">✏️</button>
<button type="button" class="mealplan-action-btn btn-delete" onclick="deleteSlot(${day.day_index}, '${mt.key}')" title="Mahlzeit löschen">✕</button>
</div>
</div>
<div class="mealplan-slot-title">
${slot.recipe_title ? `
<a href="/cookbook#rezept-${slot.recipe_id}" class="mealplan-recipe-link" onclick="closeMealPlanModal()" title="Rezept im Kochbuch aufrufen">
📖 ${escapeHtml(slot.recipe_title)}
</a>
` : `
<span>${escapeHtml(slot.custom_text)}</span>
`}
</div>
</div>
`;
});
// Falls noch nicht alle 4 Mahlzeiten belegt sind, zeige "+ Mahlzeit"
if (filledSlots.length < 4) {
html += `
<button type="button" class="mealplan-add-meal-btn" onclick="openSlotEditor(${day.day_index}, '', '${day.name}')" title="Weitere Mahlzeit planen">
<span>+</span> <span>Mahlzeit</span>
</button>
`;
}
} else {
// Tag ist leer: Kompakter Button zum Anlegen
html += `
<div class="mealplan-empty-day-state">
<button type="button" class="mealplan-add-meal-btn is-first" onclick="openSlotEditor(${day.day_index}, '', '${day.name}')" title="Mahlzeit für ${day.name} eintragen">
<span>+</span> <span>Mahlzeit planen</span>
</button>
</div>
`;
}
html += `
</div>
</div>
`;
});
container.innerHTML = html;
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ==========================================
// SLOT EDITOR & REZEPT LIVE-SUCHE
// ==========================================
function openSlotEditor(dayIndex, mealType, dayName) {
document.getElementById('se-day').value = dayIndex;
// Falls mealType leer ist, wähle automatisch den nächsten freien Slot
if (!mealType) {
const order = ['mittag', 'kaffee', 'abend', 'frueh'];
mealType = order.find(t => {
const k = `${dayIndex}_${t}`;
return !(mpData && mpData.slots && mpData.slots[k]);
}) || 'mittag';
}
document.getElementById('slot-editor-title').textContent = `🍽️ ${dayName || 'Tag'}: Mahlzeit planen`;
// Mahlzeittyp setzen & Felder füllen
selectSlotMealType(mealType, false);
document.getElementById('modal-slot-editor').classList.add('active');
document.body.classList.add('modal-open');
}
function selectSlotMealType(mealType, userClicked = true) {
document.getElementById('se-type').value = mealType;
// Aktiven Chip hervorheben
document.querySelectorAll('.slot-type-chip').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-type') === mealType);
});
// Bestehende Werte für diesen Tag + mealType prüfen
const dayIndex = parseInt(document.getElementById('se-day').value, 10);
const slotKey = `${dayIndex}_${mealType}`;
const current = mpData && mpData.slots ? mpData.slots[slotKey] : null;
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()))) {
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) {
clearSlotRecipeSelection();
customInput.value = '';
}
if (delBtn) delBtn.style.display = 'none';
}
}
function onSlotRecipeSearchInput(val) {
const dropdown = document.getElementById('se-recipe-search-dropdown');
const term = (val || '').trim().toLowerCase();
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')) {
document.body.classList.remove('modal-open');
}
}
function rollRandomRecipeForSlot() {
if (!mpData || !mpData.recipes || mpData.recipes.length === 0) return;
const rand = mpData.recipes[Math.floor(Math.random() * mpData.recipes.length)];
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-id').value;
const customVal = document.getElementById('se-custom-text').value.trim();
const payload = {
year: mpCurrentYear,
week: mpCurrentWeek,
day_of_week: dayIndex,
meal_type: mealType,
recipe_id: recipeVal ? parseInt(recipeVal, 10) : null,
custom_text: (!recipeVal && customVal) ? customVal : (recipeVal ? null : '')
};
try {
if (!payload.recipe_id && !payload.custom_text) {
await fetch(`/api/mealplan/slot?year=${mpCurrentYear}&week=${mpCurrentWeek}&day_of_week=${dayIndex}&meal_type=${mealType}`, { method: 'DELETE' });
} else {
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 fehlgeschlagen');
}
closeSlotEditor();
showToast('💾 Mahlzeit gespeichert');
loadMealPlan(mpCurrentYear, mpCurrentWeek);
} catch (err) {
alert(err.message);
}
}
async function deleteCurrentSlotFromEditor() {
const dayIndex = parseInt(document.getElementById('se-day').value, 10);
const mealType = document.getElementById('se-type').value;
closeSlotEditor();
await deleteSlot(dayIndex, mealType);
}
async function deleteSlot(dayIndex, mealType) {
try {
await fetch(`/api/mealplan/slot?year=${mpCurrentYear}&week=${mpCurrentWeek}&day_of_week=${dayIndex}&meal_type=${mealType}`, { method: 'DELETE' });
showToast('🗑️ Slot geleert');
loadMealPlan(mpCurrentYear, mpCurrentWeek);
} catch (err) {
alert(err.message);
}
}
async function clearCurrentMealPlanWeek() {
if (!confirm(`Möchtest du den gesamten Wochenplan für KW ${mpCurrentWeek} leeren?`)) return;
try {
await fetch(`/api/mealplan/week?year=${mpCurrentYear}&week=${mpCurrentWeek}`, { method: 'DELETE' });
showToast(`🗑️ KW ${mpCurrentWeek} geleert`);
loadMealPlan(mpCurrentYear, mpCurrentWeek);
} catch (err) {
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: cb.getAttribute('data-recipe') || 'Wochenplan'
}));
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();
}
});
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(err => console.log('SW register fail:', err));
});
}
</script>
</body>
</html>