Files
cookbook/app/templates/cookbook.html
T

1878 lines
78 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="stylesheet" href="/static/cookbook.css?v=4.4">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📖</text></svg>">
<style id="print-engine-styles">
/* Auf dem Bildschirm: Cover, Inhaltsverzeichnis und Seitenzahlen ausblenden */
@media screen {
.page.cover,
.page.toc-page,
.page-footer {
display: none !important;
}
}
/* Im Druck: Millimetergenaue DIN A4 Seitenhöhe (297mm) auf Apple, Android & Desktop */
@media print {
html, body.cookbook-body {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
height: auto !important;
min-height: auto !important;
overflow: visible !important;
background-color: var(--creme) !important;
}
.book-wrapper {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
margin: 0 !important;
padding: 0 !important;
gap: 0 !important;
}
.page {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
height: 282mm !important;
min-height: 282mm !important;
max-height: 282mm !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
background-color: var(--creme) !important;
overflow: hidden !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
display: flex !important;
flex-direction: column !important;
}
.page.cover {
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
height: 282mm !important;
min-height: 282mm !important;
max-height: 282mm !important;
padding: 16mm 10mm !important;
box-sizing: border-box !important;
overflow: hidden !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
}
.cover-box {
padding: 16mm 14mm !important;
max-width: 85% !important;
}
.page.toc-page {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
height: 282mm !important;
min-height: 282mm !important;
max-height: 282mm !important;
padding: 12mm 10mm !important;
box-sizing: border-box !important;
overflow: hidden !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
}
.divider {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
height: 282mm !important;
min-height: 282mm !important;
max-height: 282mm !important;
padding: 14mm 10mm !important;
box-sizing: border-box !important;
position: relative !important;
overflow: hidden !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
}
.doodle-placeholder {
width: 140mm !important;
height: 140mm !important;
max-width: 140mm !important;
max-height: 140mm !important;
margin: 6mm auto 0 auto !important;
border: none !important;
background: transparent !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
}
.doodle-placeholder img {
width: 100% !important;
height: 100% !important;
max-width: 100% !important;
max-height: 100% !important;
object-fit: contain !important;
}
.recipe-page {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
height: 282mm !important;
min-height: 282mm !important;
max-height: 282mm !important;
padding: 6mm 10mm !important;
box-sizing: border-box !important;
position: relative !important;
overflow: hidden !important;
page-break-after: always !important;
break-after: page !important;
page-break-inside: avoid !important;
break-inside: avoid !important;
display: flex !important;
flex-direction: column !important;
justify-content: flex-start !important;
}
.recipe-header-top {
display: none !important;
}
.recipe-source-line {
display: none !important;
}
.notes-box-empty {
display: block !important;
padding: 4px 8px !important;
margin-top: 3px !important;
background: #FFFDF0 !important;
border: 1px dashed #D4C165 !important;
}
.notes-blank-lines {
display: flex !important;
flex-direction: column !important;
gap: 8px !important;
margin-top: 3px !important;
}
.notes-blank-line {
border-bottom: 1px dotted rgba(100, 90, 70, 0.4) !important;
height: 10px !important;
width: 100% !important;
}
.print-only {
display: block !important;
}
.no-print {
display: none !important;
}
.recipe-grid {
grid-template-columns: 58mm 1fr !important;
gap: 8mm !important;
width: 100% !important;
min-width: 0 !important;
flex: 1 1 auto !important;
}
.page-footer {
display: flex !important;
margin-top: auto !important;
position: relative !important;
bottom: auto !important;
border: none !important;
padding: 3mm 0 2mm 0 !important;
width: 100% !important;
box-sizing: border-box !important;
z-index: 100 !important;
}
.page-footer.page-even {
left: auto !important;
right: auto !important;
justify-content: flex-start !important;
text-align: left !important;
padding-left: 4mm !important;
padding-right: 0 !important;
}
.page-footer.page-odd {
right: auto !important;
left: auto !important;
justify-content: flex-end !important;
text-align: right !important;
padding-right: 4mm !important;
padding-left: 0 !important;
}
.page-footer .page-number {
font-size: 0.95rem !important;
font-weight: 700 !important;
color: var(--tannengruen) !important;
}
.page:last-child {
page-break-after: auto !important;
break-after: auto !important;
}
}
</style>
<script>
(function() {
var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
(window.CSS && CSS.supports && CSS.supports('-webkit-touch-callout', 'none'));
if (isIOS) {
document.documentElement.classList.add('is-ios');
if (document.body) document.body.classList.add('is-ios');
document.addEventListener('DOMContentLoaded', function() {
if (document.body) document.body.classList.add('is-ios');
});
}
})();
</script>
</head>
<body class="cookbook-body {% if is_ios %}is-ios{% endif %}">
<!-- 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>
<!-- 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="/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="/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">
<!-- 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 > 20 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">
{% 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 }}">
{% 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">💡 Eigene Notizen & Tipps</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">
{% 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>
<!-- ========================================== -->
<!-- MODAL 0: WOCHENMENÜ-PLAN (no-print) -->
<!-- ========================================== -->
<div id="modal-mealplan" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-mealplan')">
<div class="cb-modal-card" style="max-width: 960px;">
<div class="cb-modal-header">
<h3 class="cb-modal-title"><span>📅</span> Wochenmenü-Plan</h3>
<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: 6px;">
<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, 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>
<div style="margin-bottom: 14px;">
<label class="cb-label">Rezept aus dem Kochbuch wählen:</label>
<select id="se-recipe-select" class="cb-select" onchange="onSlotRecipeSelect(this.value)">
<option value="">-- Kein Rezept / Freitext nutzen --</option>
</select>
</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">
</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 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');
}
});
// ==========================================
// 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();
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
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 }};
let genSlotCount = 2;
let genSlots = [
{ category: "", efforts: [] },
{ category: "", efforts: [] },
{ category: "", efforts: [] },
{ category: "", efforts: [] },
{ category: "", efforts: [] }
];
let genCurrentResults = [];
function setGenSlotCount(count) {
genSlotCount = count;
document.querySelectorAll('.gen-count-btn').forEach((btn, idx) => {
btn.classList.toggle('active', idx + 1 === count);
});
renderGenSlots();
}
function initGeneratorSlots() {
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 ? GEN_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 -->
<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 Kapitel (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 Kapitel / Beliebig</div>
<div style="font-size: 0.8rem; color: #666;">Zufall aus dem gesamten Kochbuch</div>
</div>
<span class="chapter-recipe-count">${TOTAL_RECIPES} Rezepte</span>
</div>
${GEN_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() {
const sec = document.getElementById('gen-results-section');
const list = document.getElementById('gen-results-list');
sec.style.display = 'block';
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">
<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>
</div>
</div>
`;
}).join('');
}
function jumpToRecipe(recipeId, e) {
if (e) e.preventDefault();
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}`;
}
}
// ==========================================
// 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
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`;
// Rezept-Dropdown initialisieren
populateSlotRecipeDropdown();
// 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 => {
if (btn.getAttribute('data-type') === mealType) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// 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 sel = document.getElementById('se-recipe-select');
const customInput = document.getElementById('se-custom-text');
const delBtn = document.getElementById('se-btn-delete');
if (current && (current.recipe_title || (current.custom_text && current.custom_text.trim()))) {
sel.value = current.recipe_id || '';
customInput.value = current.custom_text || '';
if (delBtn) delBtn.style.display = 'inline-block';
} else {
if (userClicked) {
sel.value = '';
customInput.value = '';
}
if (delBtn) delBtn.style.display = 'none';
}
}
function populateSlotRecipeDropdown() {
const sel = document.getElementById('se-recipe-select');
if (sel.options.length > 1) return; // bereits befüllt
sel.innerHTML = '<option value="">-- Kein Rezept / Freitext nutzen --</option>';
if (mpData && mpData.recipes) {
const cats = {};
mpData.recipes.forEach(r => {
if (!cats[r.category]) cats[r.category] = [];
cats[r.category].push(r);
});
Object.keys(cats).sort().forEach(cat => {
const optGroup = document.createElement('optgroup');
optGroup.label = cat;
cats[cat].sort((a,b) => a.title.localeCompare(b.title)).forEach(r => {
const opt = document.createElement('option');
opt.value = r.id;
opt.textContent = `${r.title} (${r.effort})`;
optGroup.appendChild(opt);
});
sel.appendChild(optGroup);
});
}
}
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 onSlotRecipeSelect(val) {
if (val) {
document.getElementById('se-custom-text').value = '';
}
}
function rollRandomRecipeForSlot() {
if (!mpData || !mpData.recipes || mpData.recipes.length === 0) return;
const rand = mpData.recipes[Math.floor(Math.random() * mpData.recipes.length)];
const sel = document.getElementById('se-recipe-select');
sel.value = rand.id;
document.getElementById('se-custom-text').value = '';
showToast(`🎲 Ausgewählt: ${rand.title}`);
}
async function saveSlotEditor() {
const dayIndex = parseInt(document.getElementById('se-day').value, 10);
const mealType = document.getElementById('se-type').value;
const recipeVal = document.getElementById('se-recipe-select').value;
const 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);
}
}
</script>
</body>
</html>