1878 lines
78 KiB
HTML
Executable File
1878 lines
78 KiB
HTML
Executable File
<!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">×</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()">×</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">×</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">×</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 125 g Butter 3 Eier 250 ml Milch 1 Pck. Vanillezucker 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. Eier einzeln unterrühren. Mehl mit Backpulver und Milch abwechselnd hinzugeben. 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">×</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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// 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>
|