Feature: Einstiegsseite, Einkaufsliste, Wochenplan-Live-Suche & Zufallsgenerator-Optimierung
This commit is contained in:
+589
-61
@@ -488,6 +488,12 @@
|
||||
|
||||
<!-- Desktop Buttons (auf Bildschirmen > 768px) -->
|
||||
<div class="cb-nav-actions desktop-nav-actions">
|
||||
<a href="/" class="cb-nav-btn" title="Menü-Übersicht öffnen">
|
||||
<span>🏠</span> <span>Menü</span>
|
||||
</a>
|
||||
<a href="/shopping-list" class="cb-nav-btn" title="Einkaufsliste öffnen">
|
||||
<span>🛒</span> <span>Einkaufsliste</span>
|
||||
</a>
|
||||
<button type="button" onclick="openMealPlanModal(event)" class="cb-nav-btn" title="Wochenmenü-Plan öffnen">
|
||||
<span>📅</span> <span>Wochenplan</span>
|
||||
</button>
|
||||
@@ -511,6 +517,12 @@
|
||||
<span>⋮</span>
|
||||
</button>
|
||||
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
|
||||
<a href="/" class="cb-mobile-item">
|
||||
<span>🏠</span> <span>Menü-Übersicht</span>
|
||||
</a>
|
||||
<a href="/shopping-list" class="cb-mobile-item">
|
||||
<span>🛒</span> <span>Einkaufsliste</span>
|
||||
</a>
|
||||
<button type="button" onclick="handleMobileMenuAction(openMealPlanModal)" class="cb-mobile-item">
|
||||
<span>📅</span> <span>Wochenmenü-Plan</span>
|
||||
</button>
|
||||
@@ -873,13 +885,27 @@
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Floating Return Banner: Zurück zum Zufallsgenerator (wenn von dort weitergeleitet) -->
|
||||
<div id="shuffle-return-banner" class="shuffle-return-banner no-print" style="display: none;">
|
||||
<button type="button" class="shuffle-return-btn" onclick="returnToShuffle()">
|
||||
<span style="font-size: 1.2rem;">🎲</span>
|
||||
<span>Zurück zum Zufallsgenerator</span>
|
||||
</button>
|
||||
<button type="button" class="shuffle-return-dismiss" onclick="dismissShuffleBanner()" title="Banner ausblenden">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL 0: WOCHENMENÜ-PLAN (no-print) -->
|
||||
<!-- ========================================== -->
|
||||
<div id="modal-mealplan" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-mealplan')">
|
||||
<div class="cb-modal-card" style="max-width: 960px;">
|
||||
<div class="cb-modal-header">
|
||||
<h3 class="cb-modal-title"><span>📅</span> Wochenmenü-Plan</h3>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<h3 class="cb-modal-title" style="margin: 0;"><span>📅</span> Wochenmenü-Plan</h3>
|
||||
<button type="button" id="mp-btn-back-to-shuffle" class="cb-btn cb-btn-outline" onclick="returnToShuffleFromMealPlan()" style="display: none; color: var(--rostrot); border-color: var(--rostrot); font-size: 0.82rem; padding: 4px 10px;">
|
||||
🎲 Zurück zum Shuffle
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="cb-modal-close" onclick="closeMealPlanModal()" title="Schließen">×</button>
|
||||
</div>
|
||||
<div class="cb-modal-body">
|
||||
@@ -904,7 +930,10 @@
|
||||
<span id="mp-today-pill" class="mealplan-today-tag" style="display: none;">Aktuelle Woche</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 6px;">
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button type="button" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme); font-size: 0.85rem; padding: 5px 12px; display: inline-flex; align-items: center; gap: 6px;" onclick="openMealPlanIngredientsModal()" title="Zutaten dieser Woche auf die Einkaufsliste setzen">
|
||||
<span>🛒</span> <span>Zutaten auf Einkaufsliste</span>
|
||||
</button>
|
||||
<button type="button" class="cb-btn cb-btn-outline" style="color: #666; border-color: #ccc; font-size: 0.85rem; padding: 5px 10px;" onclick="clearCurrentMealPlanWeek()" title="Ganze Woche leeren">
|
||||
🗑️ Woche leeren
|
||||
</button>
|
||||
@@ -931,7 +960,7 @@
|
||||
<input type="hidden" id="se-day">
|
||||
<input type="hidden" id="se-type" value="mittag">
|
||||
|
||||
<!-- Mahlzeittyp-Auswahl: Früh, Mittag, Abend -->
|
||||
<!-- Mahlzeittyp-Auswahl: Früh, Mittag, Kaffee, Abend -->
|
||||
<div style="margin-bottom: 14px;">
|
||||
<label class="cb-label">Mahlzeit:</label>
|
||||
<div class="slot-type-chips">
|
||||
@@ -942,18 +971,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 14px;">
|
||||
<label class="cb-label">Rezept aus dem Kochbuch wählen:</label>
|
||||
<select id="se-recipe-select" class="cb-select" onchange="onSlotRecipeSelect(this.value)">
|
||||
<option value="">-- Kein Rezept / Freitext nutzen --</option>
|
||||
</select>
|
||||
<!-- Live-Suchfeld für Rezepte statt starrer Dropdown -->
|
||||
<div class="slot-recipe-search-container">
|
||||
<label class="cb-label" for="se-recipe-search-input">Rezept aus dem Kochbuch suchen:</label>
|
||||
<div class="slot-search-input-wrapper">
|
||||
<input type="search" id="se-recipe-search-input" class="cb-input"
|
||||
placeholder="🔍 Rezept suchen (z. B. Waffeln, Chili, Suppe...)"
|
||||
oninput="onSlotRecipeSearchInput(this.value)"
|
||||
onfocus="onSlotRecipeSearchFocus()"
|
||||
autocomplete="off">
|
||||
<input type="hidden" id="se-recipe-id" value="">
|
||||
</div>
|
||||
|
||||
<!-- Badge für ausgewähltes Rezept -->
|
||||
<div id="se-selected-recipe-badge" style="display: none; margin-top: 8px; background: rgba(30, 77, 59, 0.08); border: 1.5px solid var(--tannengruen); border-radius: 6px; padding: 6px 12px; justify-content: space-between; align-items: center;">
|
||||
<span style="font-weight: 700; color: var(--tannengruen); font-size: 0.92rem;">
|
||||
✅ Ausgewählt: <span id="se-selected-recipe-name"></span>
|
||||
</span>
|
||||
<button type="button" onclick="clearSlotRecipeSelection()" style="background: transparent; border: none; color: #c53030; font-weight: 700; cursor: pointer; font-size: 0.85rem;">
|
||||
✕ Entfernen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Live Suchergebnisse Dropdown -->
|
||||
<div id="se-recipe-search-dropdown" class="slot-search-dropdown" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 10px 0; font-family: var(--font-hand); color: #888;">— ODER —</div>
|
||||
|
||||
<div style="margin-bottom: 16px;">
|
||||
<label class="cb-label">Freitext (z. B. Restaurant, Reste, Brotzeit):</label>
|
||||
<input type="text" id="se-custom-text" class="cb-input" placeholder="z. B. Essen gehen, Brotzeit, Pizza" autocomplete="off">
|
||||
<input type="text" id="se-custom-text" class="cb-input" placeholder="z. B. Essen gehen, Brotzeit, Pizza" autocomplete="off" oninput="onSlotCustomTextInput(this.value)">
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 18px; padding-top: 12px; border-top: 1.5px dashed rgba(34,34,34,0.15); flex-wrap: wrap; gap: 8px;">
|
||||
@@ -978,6 +1026,94 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL: WOCHENPLAN ZUTATEN AUF EINKAUFSLISTE-->
|
||||
<!-- ========================================== -->
|
||||
<div id="modal-mealplan-ingredients" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-mealplan-ingredients')" style="z-index: 2300;">
|
||||
<div class="cb-modal-card" style="max-width: 640px;">
|
||||
<div class="cb-modal-header">
|
||||
<h3 class="cb-modal-title"><span>🛒</span> Zutaten für die Einkaufsliste</h3>
|
||||
<button type="button" class="cb-modal-close" onclick="closeMealPlanIngredientsModal()" title="Schließen">×</button>
|
||||
</div>
|
||||
<div class="cb-modal-body">
|
||||
<p style="font-size: 0.92rem; color: #555; margin: 0 0 12px 0;">
|
||||
Wähle die Zutaten der geplanten Gerichte für <strong id="mpi-week-label">diese Woche</strong> aus, die du auf die Einkaufsliste übertragen möchtest:
|
||||
</p>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding: 8px 12px; background: #fff; border: 1.5px solid rgba(34,34,34,0.15); border-radius: 8px;">
|
||||
<label style="display: flex; align-items: center; gap: 8px; font-size: 0.9rem; font-weight: 700; cursor: pointer; color: var(--tannengruen); margin: 0;">
|
||||
<input type="checkbox" id="mpi-toggle-all" checked onchange="toggleAllMpiCheckboxes(this.checked)" style="width: 18px; height: 18px; accent-color: var(--tannengruen);">
|
||||
<span>Alle auswählen / abwählen</span>
|
||||
</label>
|
||||
<span id="mpi-total-count" style="font-size: 0.85rem; color: #666; font-weight: 600;">0 Zutaten</span>
|
||||
</div>
|
||||
|
||||
<!-- Scrollbare Zutaten-Liste -->
|
||||
<div id="mpi-ingredients-container" style="max-height: 380px; overflow-y: auto; padding-right: 4px;">
|
||||
<!-- Dynamisch via JS -->
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 18px; padding-top: 12px; border-top: 1.5px dashed rgba(34,34,34,0.15); flex-wrap: wrap; gap: 8px;">
|
||||
<button type="button" onclick="closeMealPlanIngredientsModal()" class="cb-btn cb-btn-modal-cancel">
|
||||
Abbrechen
|
||||
</button>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<a href="/shopping-list" class="cb-btn cb-btn-outline" style="color: var(--schwarz); border-color: rgba(34,34,34,0.3); font-size: 0.9rem;">
|
||||
<span>🛒</span> Zur Einkaufsliste
|
||||
</a>
|
||||
<button type="button" id="mpi-btn-transfer" onclick="transferMpiToShoppingList()" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme); font-weight: 700; font-size: 0.95rem; padding: 8px 18px;">
|
||||
<span>➕</span> <span>Ausgewählte übertragen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL: SHUFFLE-GERICHT IN WOCHENPLAN -->
|
||||
<!-- ========================================== -->
|
||||
<div id="modal-shuffle-assign" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-shuffle-assign')" style="z-index: 2250;">
|
||||
<div class="cb-modal-card" style="max-width: 440px;">
|
||||
<div class="cb-modal-header">
|
||||
<h4 class="cb-modal-title"><span>📅</span> In Wochenplan einfügen</h4>
|
||||
<button type="button" class="cb-modal-close" onclick="closeShuffleAssignModal()">×</button>
|
||||
</div>
|
||||
<div class="cb-modal-body">
|
||||
<input type="hidden" id="sa-recipe-id">
|
||||
<div style="margin-bottom: 14px; background: rgba(30,77,59,0.06); padding: 10px 14px; border-radius: 8px; border: 1.5px solid rgba(30,77,59,0.2);">
|
||||
<span style="font-size: 0.8rem; color: #666; display: block; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700;">Rezept:</span>
|
||||
<span id="sa-recipe-title" style="font-size: 1.15rem; font-weight: 700; color: var(--tannengruen); display: block; margin-top: 2px;"></span>
|
||||
</div>
|
||||
<div class="cb-form-group">
|
||||
<label class="cb-label">Wochentag wählen:</label>
|
||||
<select id="sa-day-select" class="cb-select">
|
||||
<option value="0">Montag</option>
|
||||
<option value="1">Dienstag</option>
|
||||
<option value="2">Mittwoch</option>
|
||||
<option value="3">Donnerstag</option>
|
||||
<option value="4">Freitag</option>
|
||||
<option value="5">Samstag</option>
|
||||
<option value="6">Sonntag</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="cb-form-group">
|
||||
<label class="cb-label">Mahlzeit:</label>
|
||||
<select id="sa-meal-type-select" class="cb-select">
|
||||
<option value="mittag" selected>☀️ Mittag</option>
|
||||
<option value="abend">🌙 Abend</option>
|
||||
<option value="kaffee">☕ Kaffee & Kuchen</option>
|
||||
<option value="frueh">🌅 Früh</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; padding-top: 12px; border-top: 1px solid #ddd;">
|
||||
<button type="button" onclick="closeShuffleAssignModal()" class="cb-btn cb-btn-modal-cancel">Abbrechen</button>
|
||||
<button type="button" onclick="saveShuffleToMealPlan()" class="cb-btn cb-btn-modal-submit">💾 Eintragen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL 1: ESSENS-ZUFALLSGENERATOR (no-print)-->
|
||||
<!-- ========================================== -->
|
||||
@@ -1438,11 +1574,15 @@
|
||||
if (modalId === 'modal-generator') closeGeneratorModal();
|
||||
if (modalId === 'modal-recipe') closeRecipeModal();
|
||||
if (modalId === 'modal-print') closePrintModal();
|
||||
if (modalId === 'modal-mealplan-ingredients') closeMealPlanIngredientsModal();
|
||||
if (modalId === 'modal-shuffle-assign') closeShuffleAssignModal();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeShuffleAssignModal();
|
||||
closeMealPlanIngredientsModal();
|
||||
closeSlotEditor();
|
||||
closeMealPlanModal();
|
||||
closeGeneratorModal();
|
||||
@@ -1476,6 +1616,10 @@
|
||||
];
|
||||
const TOTAL_RECIPES = {{ total_recipes }};
|
||||
|
||||
// Filter für Zufallsgenerator: Kategorien Desserts (9), Kuchen & Gebäck (10) und Plätzchen (11) ausschließen
|
||||
const SHUFFLE_CHAPTERS = GEN_CHAPTERS.filter(c => c.number !== 9 && c.number !== 10 && c.number !== 11);
|
||||
const SHUFFLE_TOTAL_RECIPES = SHUFFLE_CHAPTERS.reduce((s, c) => s + (c.count || 0), 0);
|
||||
|
||||
let genSlotCount = 2;
|
||||
let genSlots = [
|
||||
{ category: "", efforts: [] },
|
||||
@@ -1486,15 +1630,55 @@
|
||||
];
|
||||
let genCurrentResults = [];
|
||||
|
||||
function saveShuffleStateToStorage() {
|
||||
try {
|
||||
sessionStorage.setItem('wtc_shuffle_results', JSON.stringify(genCurrentResults || []));
|
||||
sessionStorage.setItem('wtc_shuffle_slots', JSON.stringify(genSlots || []));
|
||||
sessionStorage.setItem('wtc_shuffle_count', String(genSlotCount));
|
||||
} catch (e) {
|
||||
console.warn('Fehler beim Speichern des Shuffle-Zustands:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreShuffleStateFromStorage() {
|
||||
try {
|
||||
const storedResults = sessionStorage.getItem('wtc_shuffle_results');
|
||||
const storedSlots = sessionStorage.getItem('wtc_shuffle_slots');
|
||||
const storedCount = sessionStorage.getItem('wtc_shuffle_count');
|
||||
|
||||
if (storedSlots) {
|
||||
genSlots = JSON.parse(storedSlots);
|
||||
}
|
||||
if (storedCount) {
|
||||
genSlotCount = parseInt(storedCount, 10) || 2;
|
||||
document.querySelectorAll('.gen-count-btn').forEach((btn, idx) => {
|
||||
btn.classList.toggle('active', idx + 1 === genSlotCount);
|
||||
});
|
||||
}
|
||||
if (storedResults) {
|
||||
genCurrentResults = JSON.parse(storedResults);
|
||||
if (genCurrentResults.length > 0) {
|
||||
renderGenResults(false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Fehler beim Wiederherstellen des Shuffle-Zustands:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function setGenSlotCount(count) {
|
||||
genSlotCount = count;
|
||||
document.querySelectorAll('.gen-count-btn').forEach((btn, idx) => {
|
||||
btn.classList.toggle('active', idx + 1 === count);
|
||||
});
|
||||
renderGenSlots();
|
||||
saveShuffleStateToStorage();
|
||||
}
|
||||
|
||||
function initGeneratorSlots() {
|
||||
if (genCurrentResults.length === 0 && sessionStorage.getItem('wtc_shuffle_results')) {
|
||||
restoreShuffleStateFromStorage();
|
||||
}
|
||||
renderGenSlots();
|
||||
}
|
||||
|
||||
@@ -1503,7 +1687,7 @@
|
||||
let html = '';
|
||||
for (let i = 0; i < genSlotCount; i++) {
|
||||
const slot = genSlots[i];
|
||||
const selectedChap = slot.category ? GEN_CHAPTERS.find(c => c.number == slot.category) : null;
|
||||
const selectedChap = slot.category ? SHUFFLE_CHAPTERS.find(c => c.number == slot.category) : null;
|
||||
const isAllEfforts = !slot.efforts || slot.efforts.length === 0;
|
||||
const isGering = slot.efforts && slot.efforts.includes('gering');
|
||||
const isMittel = slot.efforts && slot.efforts.includes('mittel');
|
||||
@@ -1515,7 +1699,7 @@
|
||||
<span>🍽️ Gericht ${i + 1}</span>
|
||||
</div>
|
||||
<div class="cb-row" style="gap: 12px; align-items: flex-start;">
|
||||
<!-- Kapitel Dropdown ohne "Kapitel X" Präfix -->
|
||||
<!-- Kapitel Dropdown ohne "Kapitel X" Präfix & ohne Desserts/Kuchen -->
|
||||
<div class="cb-col" style="flex: 1.6; min-width: 180px;">
|
||||
<label style="font-size: 0.85rem; color: #555; font-weight: 700; margin-bottom: 4px; display: block;">Kategorie / Kapitel:</label>
|
||||
<div class="custom-chapter-select" id="custom-chapter-select-${i}">
|
||||
@@ -1526,7 +1710,7 @@
|
||||
<span class="chapter-select-btn-title">${selectedChap.title}</span>
|
||||
` : `
|
||||
<span style="font-size: 1.25rem; line-height: 1;">🎲</span>
|
||||
<span class="chapter-select-btn-title">Alle Kapitel (Beliebig)</span>
|
||||
<span class="chapter-select-btn-title">Alle Hauptgerichte (Beliebig)</span>
|
||||
`}
|
||||
</div>
|
||||
<span class="chapter-select-arrow">▼</span>
|
||||
@@ -1535,12 +1719,12 @@
|
||||
<div class="chapter-option ${!slot.category ? 'is-selected' : ''}" onclick="selectSlotChapter(${i}, '')">
|
||||
<span style="font-size: 1.3rem; line-height: 1; width: 32px; text-align: center;">🎲</span>
|
||||
<div class="chapter-option-text">
|
||||
<div class="chapter-option-title">Alle Kapitel / Beliebig</div>
|
||||
<div style="font-size: 0.8rem; color: #666;">Zufall aus dem gesamten Kochbuch</div>
|
||||
<div class="chapter-option-title">Alle Hauptgerichte / Beliebig</div>
|
||||
<div style="font-size: 0.8rem; color: #666;">Zufall aus allen herzhaften Kategorien</div>
|
||||
</div>
|
||||
<span class="chapter-recipe-count">${TOTAL_RECIPES} Rezepte</span>
|
||||
<span class="chapter-recipe-count">${SHUFFLE_TOTAL_RECIPES} Rezepte</span>
|
||||
</div>
|
||||
${GEN_CHAPTERS.map(c => `
|
||||
${SHUFFLE_CHAPTERS.map(c => `
|
||||
<div class="chapter-option ${slot.category == c.number ? 'is-selected' : ''}" onclick="selectSlotChapter(${i}, ${c.number})">
|
||||
<img src="${c.illustration}" class="chapter-option-thumb" alt="${c.title}">
|
||||
<div class="chapter-option-text">
|
||||
@@ -1682,11 +1866,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderGenResults() {
|
||||
function renderGenResults(saveState = true) {
|
||||
const sec = document.getElementById('gen-results-section');
|
||||
const list = document.getElementById('gen-results-list');
|
||||
sec.style.display = 'block';
|
||||
|
||||
if (saveState) {
|
||||
saveShuffleStateToStorage();
|
||||
}
|
||||
|
||||
if (!genCurrentResults || genCurrentResults.length === 0) {
|
||||
list.innerHTML = '<p style="text-align:center; color:#666;">Keine Rezepte ausgelost.</p>';
|
||||
return;
|
||||
@@ -1743,11 +1931,15 @@
|
||||
🔄 Neu
|
||||
</button>
|
||||
</div>
|
||||
<div class="gen-actions">
|
||||
<div class="gen-actions" style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<a href="#rezept-${d.id}" onclick="jumpToRecipe(${d.id}, event)" class="cb-btn cb-btn-creme"
|
||||
style="background: var(--tannengruen); color: var(--creme); font-size: 0.95rem; padding: 6px 16px;">
|
||||
📖 Direkt zum Rezept im Buch
|
||||
</a>
|
||||
<button type="button" onclick="openShuffleToMealPlanModal(${d.id}, '${escapeHtml(d.title || d.name)}', event)" class="cb-btn cb-btn-outline"
|
||||
style="color: var(--tannengruen); border-color: var(--tannengruen); font-size: 0.95rem; padding: 6px 14px; display: inline-flex; align-items: center; gap: 6px;">
|
||||
<span>📅</span> <span>In Wochenplan</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1756,6 +1948,8 @@
|
||||
|
||||
function jumpToRecipe(recipeId, e) {
|
||||
if (e) e.preventDefault();
|
||||
sessionStorage.setItem('wtc_from_shuffle', 'true');
|
||||
checkShuffleReturnState();
|
||||
closeGeneratorModal();
|
||||
const el = document.getElementById(`rezept-${recipeId}`);
|
||||
if (el) {
|
||||
@@ -1768,6 +1962,114 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SHUFFLE RETENTION & WOCHENPLAN ÜBERTRAGUNG
|
||||
// ==========================================
|
||||
function checkShuffleReturnState() {
|
||||
const isFromShuffle = sessionStorage.getItem('wtc_from_shuffle') === 'true';
|
||||
const hasResults = !!sessionStorage.getItem('wtc_shuffle_results');
|
||||
const banner = document.getElementById('shuffle-return-banner');
|
||||
const mpBtn = document.getElementById('mp-btn-back-to-shuffle');
|
||||
|
||||
if (isFromShuffle && hasResults) {
|
||||
if (banner) banner.style.display = 'flex';
|
||||
if (mpBtn) mpBtn.style.display = 'inline-flex';
|
||||
} else {
|
||||
if (banner) banner.style.display = 'none';
|
||||
if (mpBtn) mpBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function returnToShuffle() {
|
||||
closeMealPlanIngredientsModal();
|
||||
closeShuffleAssignModal();
|
||||
closeSlotEditor();
|
||||
closeMealPlanModal();
|
||||
closeRecipeModal();
|
||||
closePrintModal();
|
||||
|
||||
openGeneratorModal();
|
||||
restoreShuffleStateFromStorage();
|
||||
renderGenSlots();
|
||||
}
|
||||
|
||||
function returnToShuffleFromMealPlan() {
|
||||
closeMealPlanModal();
|
||||
returnToShuffle();
|
||||
}
|
||||
|
||||
function dismissShuffleBanner() {
|
||||
sessionStorage.removeItem('wtc_from_shuffle');
|
||||
checkShuffleReturnState();
|
||||
}
|
||||
|
||||
function openShuffleToMealPlanModal(recipeId, recipeTitle, e) {
|
||||
if (e) e.stopPropagation();
|
||||
document.getElementById('sa-recipe-id').value = recipeId;
|
||||
document.getElementById('sa-recipe-title').textContent = recipeTitle || 'Rezept';
|
||||
|
||||
const jsDay = new Date().getDay();
|
||||
const isoDay = jsDay === 0 ? 6 : jsDay - 1;
|
||||
document.getElementById('sa-day-select').value = String(isoDay);
|
||||
document.getElementById('sa-meal-type-select').value = 'mittag';
|
||||
|
||||
document.getElementById('modal-shuffle-assign').classList.add('active');
|
||||
document.body.classList.add('modal-open');
|
||||
}
|
||||
|
||||
function closeShuffleAssignModal() {
|
||||
document.getElementById('modal-shuffle-assign').classList.remove('active');
|
||||
if (!document.getElementById('modal-generator').classList.contains('active') &&
|
||||
!document.getElementById('modal-mealplan').classList.contains('active')) {
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveShuffleToMealPlan() {
|
||||
const recipeId = parseInt(document.getElementById('sa-recipe-id').value, 10);
|
||||
const dayIdx = parseInt(document.getElementById('sa-day-select').value, 10);
|
||||
const mealType = document.getElementById('sa-meal-type-select').value;
|
||||
const dayName = document.getElementById('sa-day-select').options[document.getElementById('sa-day-select').selectedIndex].text;
|
||||
const mealName = document.getElementById('sa-meal-type-select').options[document.getElementById('sa-meal-type-select').selectedIndex].text;
|
||||
|
||||
let year = mpCurrentYear;
|
||||
let week = mpCurrentWeek;
|
||||
if (!year || !week) {
|
||||
const now = new Date();
|
||||
const d = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
week = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||
year = d.getUTCFullYear();
|
||||
}
|
||||
|
||||
const payload = {
|
||||
year: year,
|
||||
week: week,
|
||||
day_of_week: dayIdx,
|
||||
meal_type: mealType,
|
||||
recipe_id: recipeId,
|
||||
custom_text: null
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/mealplan/slot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) throw new Error('Speichern im Wochenplan fehlgeschlagen');
|
||||
|
||||
closeShuffleAssignModal();
|
||||
sessionStorage.setItem('wtc_from_shuffle', 'true');
|
||||
checkShuffleReturnState();
|
||||
showToast(`📅 In Wochenplan eingetragen (${dayName}, ${mealName})!`);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// REZEPT-MODAL FORM SUBMIT & BILD-KOMPRIMIERUNG
|
||||
// ==========================================
|
||||
@@ -2022,7 +2324,9 @@
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// Slot Editor
|
||||
// ==========================================
|
||||
// SLOT EDITOR & REZEPT LIVE-SUCHE
|
||||
// ==========================================
|
||||
function openSlotEditor(dayIndex, mealType, dayName) {
|
||||
document.getElementById('se-day').value = dayIndex;
|
||||
|
||||
@@ -2037,9 +2341,6 @@
|
||||
|
||||
document.getElementById('slot-editor-title').textContent = `🍽️ ${dayName || 'Tag'}: Mahlzeit planen`;
|
||||
|
||||
// Rezept-Dropdown initialisieren
|
||||
populateSlotRecipeDropdown();
|
||||
|
||||
// Mahlzeittyp setzen & Felder füllen
|
||||
selectSlotMealType(mealType, false);
|
||||
|
||||
@@ -2052,11 +2353,7 @@
|
||||
|
||||
// Aktiven Chip hervorheben
|
||||
document.querySelectorAll('.slot-type-chip').forEach(btn => {
|
||||
if (btn.getAttribute('data-type') === mealType) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
btn.classList.toggle('active', btn.getAttribute('data-type') === mealType);
|
||||
});
|
||||
|
||||
// Bestehende Werte für diesen Tag + mealType prüfen
|
||||
@@ -2064,48 +2361,123 @@
|
||||
const slotKey = `${dayIndex}_${mealType}`;
|
||||
const current = mpData && mpData.slots ? mpData.slots[slotKey] : null;
|
||||
|
||||
const sel = document.getElementById('se-recipe-select');
|
||||
const customInput = document.getElementById('se-custom-text');
|
||||
const delBtn = document.getElementById('se-btn-delete');
|
||||
|
||||
if (current && (current.recipe_title || (current.custom_text && current.custom_text.trim()))) {
|
||||
sel.value = current.recipe_id || '';
|
||||
if (current.recipe_id && current.recipe_title) {
|
||||
selectSlotRecipe(current.recipe_id, current.recipe_title);
|
||||
} else {
|
||||
clearSlotRecipeSelection();
|
||||
}
|
||||
customInput.value = current.custom_text || '';
|
||||
if (delBtn) delBtn.style.display = 'inline-block';
|
||||
} else {
|
||||
if (userClicked) {
|
||||
sel.value = '';
|
||||
clearSlotRecipeSelection();
|
||||
customInput.value = '';
|
||||
}
|
||||
if (delBtn) delBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function populateSlotRecipeDropdown() {
|
||||
const sel = document.getElementById('se-recipe-select');
|
||||
if (sel.options.length > 1) return; // bereits befüllt
|
||||
function onSlotRecipeSearchInput(val) {
|
||||
const dropdown = document.getElementById('se-recipe-search-dropdown');
|
||||
const term = (val || '').trim().toLowerCase();
|
||||
|
||||
sel.innerHTML = '<option value="">-- Kein Rezept / Freitext nutzen --</option>';
|
||||
if (mpData && mpData.recipes) {
|
||||
const cats = {};
|
||||
mpData.recipes.forEach(r => {
|
||||
if (!cats[r.category]) cats[r.category] = [];
|
||||
cats[r.category].push(r);
|
||||
});
|
||||
Object.keys(cats).sort().forEach(cat => {
|
||||
const optGroup = document.createElement('optgroup');
|
||||
optGroup.label = cat;
|
||||
cats[cat].sort((a,b) => a.title.localeCompare(b.title)).forEach(r => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = r.id;
|
||||
opt.textContent = `${r.title} (${r.effort})`;
|
||||
optGroup.appendChild(opt);
|
||||
});
|
||||
sel.appendChild(optGroup);
|
||||
});
|
||||
if (!mpData || !mpData.recipes) return;
|
||||
|
||||
let matches = [];
|
||||
if (!term) {
|
||||
matches = mpData.recipes.slice(0, 15);
|
||||
} else {
|
||||
matches = mpData.recipes.filter(r => {
|
||||
const titleMatch = (r.title || '').toLowerCase().includes(term);
|
||||
const catMatch = (r.category || '').toLowerCase().includes(term);
|
||||
return titleMatch || catMatch;
|
||||
}).slice(0, 20);
|
||||
}
|
||||
|
||||
renderSlotRecipeSearchResults(matches);
|
||||
}
|
||||
|
||||
function onSlotRecipeSearchFocus() {
|
||||
const input = document.getElementById('se-recipe-search-input');
|
||||
onSlotRecipeSearchInput(input.value);
|
||||
}
|
||||
|
||||
function renderSlotRecipeSearchResults(matches) {
|
||||
const dropdown = document.getElementById('se-recipe-search-dropdown');
|
||||
if (!matches || matches.length === 0) {
|
||||
dropdown.innerHTML = '<div style="padding: 10px; color: #888; text-align: center; font-size: 0.9rem;">Kein Rezept gefunden</div>';
|
||||
dropdown.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const effortBadge = (eff) => {
|
||||
if (eff === 'gering') return '🟢 Gering';
|
||||
if (eff === 'hoch') return '🔴 Hoch';
|
||||
return '🟡 Mittel';
|
||||
};
|
||||
|
||||
let html = '';
|
||||
matches.forEach(r => {
|
||||
html += `
|
||||
<div class="slot-search-item" onclick="selectSlotRecipe(${r.id}, '${escapeHtml(r.title)}')">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div class="slot-search-item-title">${escapeHtml(r.title)}</div>
|
||||
<div class="slot-search-item-meta">
|
||||
<span>${escapeHtml(r.category || '')}</span>
|
||||
<span>•</span>
|
||||
<span>${effortBadge(r.effort)}</span>
|
||||
${r.cook_time ? `<span>• ⏱️ ${escapeHtml(r.cook_time)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<span style="font-size: 0.85rem; color: var(--tannengruen); font-weight: 700; margin-left: 8px;">Wählen ➔</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
dropdown.innerHTML = html;
|
||||
dropdown.style.display = 'block';
|
||||
}
|
||||
|
||||
function selectSlotRecipe(recipeId, recipeTitle) {
|
||||
if (!recipeTitle && mpData && mpData.recipes) {
|
||||
const found = mpData.recipes.find(r => r.id === recipeId);
|
||||
if (found) recipeTitle = found.title;
|
||||
}
|
||||
|
||||
document.getElementById('se-recipe-id').value = recipeId;
|
||||
document.getElementById('se-recipe-search-input').value = recipeTitle || '';
|
||||
document.getElementById('se-selected-recipe-name').textContent = recipeTitle || '';
|
||||
document.getElementById('se-selected-recipe-badge').style.display = 'flex';
|
||||
document.getElementById('se-recipe-search-dropdown').style.display = 'none';
|
||||
|
||||
// Freitext leeren
|
||||
document.getElementById('se-custom-text').value = '';
|
||||
}
|
||||
|
||||
function clearSlotRecipeSelection() {
|
||||
document.getElementById('se-recipe-id').value = '';
|
||||
document.getElementById('se-recipe-search-input').value = '';
|
||||
document.getElementById('se-selected-recipe-badge').style.display = 'none';
|
||||
document.getElementById('se-recipe-search-dropdown').style.display = 'none';
|
||||
}
|
||||
|
||||
function onSlotCustomTextInput(val) {
|
||||
if (val && val.trim().length > 0) {
|
||||
clearSlotRecipeSelection();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.slot-recipe-search-container')) {
|
||||
const dropdown = document.getElementById('se-recipe-search-dropdown');
|
||||
if (dropdown) dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
function closeSlotEditor() {
|
||||
document.getElementById('modal-slot-editor').classList.remove('active');
|
||||
if (!document.getElementById('modal-mealplan').classList.contains('active')) {
|
||||
@@ -2113,25 +2485,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
function onSlotRecipeSelect(val) {
|
||||
if (val) {
|
||||
document.getElementById('se-custom-text').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function rollRandomRecipeForSlot() {
|
||||
if (!mpData || !mpData.recipes || mpData.recipes.length === 0) return;
|
||||
const rand = mpData.recipes[Math.floor(Math.random() * mpData.recipes.length)];
|
||||
const sel = document.getElementById('se-recipe-select');
|
||||
sel.value = rand.id;
|
||||
document.getElementById('se-custom-text').value = '';
|
||||
selectSlotRecipe(rand.id, rand.title);
|
||||
showToast(`🎲 Ausgewählt: ${rand.title}`);
|
||||
}
|
||||
|
||||
async function saveSlotEditor() {
|
||||
const dayIndex = parseInt(document.getElementById('se-day').value, 10);
|
||||
const mealType = document.getElementById('se-type').value;
|
||||
const recipeVal = document.getElementById('se-recipe-select').value;
|
||||
const recipeVal = document.getElementById('se-recipe-id').value;
|
||||
const customVal = document.getElementById('se-custom-text').value.trim();
|
||||
|
||||
const payload = {
|
||||
@@ -2189,6 +2553,170 @@
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// WOCHENPLAN ZUTATEN AUF EINKAUFSLISTE
|
||||
// ==========================================
|
||||
let mpiIngredientsData = [];
|
||||
|
||||
async function openMealPlanIngredientsModal() {
|
||||
if (!mpCurrentYear || !mpCurrentWeek) {
|
||||
showToast('Wochenplan noch nicht geladen');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('mpi-ingredients-container');
|
||||
const countBadge = document.getElementById('mpi-total-count');
|
||||
const weekLabel = document.getElementById('mpi-week-label');
|
||||
|
||||
weekLabel.textContent = `KW ${mpCurrentWeek}`;
|
||||
container.innerHTML = '<div style="text-align: center; padding: 30px; color: #777;"><span>⏳</span> Zutaten werden zusammengestellt...</div>';
|
||||
countBadge.textContent = 'Laden...';
|
||||
|
||||
document.getElementById('modal-mealplan-ingredients').classList.add('active');
|
||||
document.body.classList.add('modal-open');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/mealplan/ingredients?year=${mpCurrentYear}&week=${mpCurrentWeek}`);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail || 'Fehler beim Laden der Zutaten');
|
||||
|
||||
const groups = data.groups || [];
|
||||
mpiIngredientsData = groups;
|
||||
|
||||
if (groups.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div style="text-align: center; padding: 30px; color: #666;">
|
||||
<span style="font-size: 2.5rem; display: block; margin-bottom: 8px;">🍽️</span>
|
||||
<h4 style="margin: 0 0 6px 0; color: var(--tannengruen); font-family: var(--font-hand); font-size: 1.4rem;">Keine Rezepte mit Zutaten geplant</h4>
|
||||
<p style="font-size: 0.95rem; margin: 0;">Füge im Wochenplan Rezepte aus dem Kochbuch hinzu, um deren Zutaten hier automatisch auf deine Einkaufsliste zu setzen.</p>
|
||||
</div>
|
||||
`;
|
||||
countBadge.textContent = '0 Zutaten';
|
||||
return;
|
||||
}
|
||||
|
||||
let totalItemCount = 0;
|
||||
let html = '';
|
||||
|
||||
groups.forEach((grp, gIdx) => {
|
||||
totalItemCount += grp.items.length;
|
||||
html += `
|
||||
<div class="mpi-group">
|
||||
<div class="mpi-group-header">
|
||||
<span>📖 ${escapeHtml(grp.recipe_title)}</span>
|
||||
<span class="mpi-group-badge">${escapeHtml(grp.slot_label)}</span>
|
||||
</div>
|
||||
<div class="mpi-group-items">
|
||||
`;
|
||||
|
||||
grp.items.forEach((it, iIdx) => {
|
||||
const qtyStr = ((it.amount || '') + (it.unit ? ' ' + it.unit : '')).trim();
|
||||
const itemId = `mpi_cb_${gIdx}_${iIdx}`;
|
||||
html += `
|
||||
<label class="mpi-item" id="mpi-label-${itemId}">
|
||||
<input type="checkbox" class="mpi-checkbox" id="${itemId}" checked
|
||||
data-recipe="${escapeHtml(grp.recipe_title)}"
|
||||
data-name="${escapeHtml(it.name)}"
|
||||
data-amount="${escapeHtml(it.amount || '')}"
|
||||
data-unit="${escapeHtml(it.unit || '')}"
|
||||
onchange="onMpiItemToggle(this, '${itemId}')">
|
||||
<span class="mpi-item-name">${escapeHtml(it.name)}</span>
|
||||
${qtyStr ? `<span class="mpi-item-qty">${escapeHtml(qtyStr)}</span>` : ''}
|
||||
</label>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
countBadge.textContent = `${totalItemCount} ${totalItemCount === 1 ? 'Zutat' : 'Zutaten'}`;
|
||||
document.getElementById('mpi-toggle-all').checked = true;
|
||||
} catch (err) {
|
||||
container.innerHTML = `<div style="text-align: center; padding: 20px; color: #c53030;">❌ ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function closeMealPlanIngredientsModal() {
|
||||
document.getElementById('modal-mealplan-ingredients').classList.remove('active');
|
||||
if (!document.getElementById('modal-mealplan').classList.contains('active')) {
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAllMpiCheckboxes(checked) {
|
||||
document.querySelectorAll('.mpi-checkbox').forEach(cb => {
|
||||
cb.checked = checked;
|
||||
const parent = cb.closest('.mpi-item');
|
||||
if (parent) parent.classList.toggle('is-unchecked', !checked);
|
||||
});
|
||||
}
|
||||
|
||||
function onMpiItemToggle(cb, id) {
|
||||
const parent = cb.closest('.mpi-item');
|
||||
if (parent) parent.classList.toggle('is-unchecked', !cb.checked);
|
||||
}
|
||||
|
||||
async function transferMpiToShoppingList() {
|
||||
const btn = document.getElementById('mpi-btn-transfer');
|
||||
const checkedBoxes = Array.from(document.querySelectorAll('.mpi-checkbox:checked'));
|
||||
|
||||
if (checkedBoxes.length === 0) {
|
||||
alert('Bitte wähle mindestens eine Zutat aus.');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span>⏳</span> <span>Wird übertragen...</span>';
|
||||
|
||||
const items = checkedBoxes.map(cb => ({
|
||||
item_name: cb.getAttribute('data-name'),
|
||||
amount: cb.getAttribute('data-amount') || '',
|
||||
unit: cb.getAttribute('data-unit') || '',
|
||||
source: `Wochenplan KW ${mpCurrentWeek} (${cb.getAttribute('data-recipe')})`
|
||||
}));
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/shopping-list/bulk', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: items })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail || 'Fehler beim Übertragen');
|
||||
|
||||
closeMealPlanIngredientsModal();
|
||||
showToast(`🛒 ${data.added_count} Zutaten auf die Einkaufsliste gesetzt!`);
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>➕</span> <span>Ausgewählte übertragen</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// INITIALISIERUNG BEIM SEITENAUFRUF
|
||||
// ==========================================
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 1. Prüfe Rückkehr-Banner vom Shuffle
|
||||
checkShuffleReturnState();
|
||||
|
||||
// 2. Prüfe Query-Parameter open=...
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const openParam = urlParams.get('open');
|
||||
if (openParam === 'generator' || openParam === 'shuffle') {
|
||||
openGeneratorModal();
|
||||
} else if (openParam === 'mealplan' || openParam === 'wochenplan') {
|
||||
openMealPlanModal();
|
||||
} else if (openParam === 'new-recipe' || openParam === 'recipe') {
|
||||
openRecipeModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user