Feature: Einstiegsseite, Einkaufsliste, Wochenplan-Live-Suche & Zufallsgenerator-Optimierung

This commit is contained in:
2026-10-15 13:05:08 +02:00
parent 2b8c4a35cf
commit 0d6d0deb69
6 changed files with 2240 additions and 67 deletions
+589 -61
View File
@@ -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">&times;</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">&times;</button>
</div>
<div class="cb-modal-body">
<p style="font-size: 0.92rem; color: #555; margin: 0 0 12px 0;">
Wähle die Zutaten der geplanten Gerichte für <strong id="mpi-week-label">diese Woche</strong> aus, die du auf die Einkaufsliste übertragen möchtest:
</p>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding: 8px 12px; background: #fff; border: 1.5px solid rgba(34,34,34,0.15); border-radius: 8px;">
<label style="display: flex; align-items: center; gap: 8px; font-size: 0.9rem; font-weight: 700; cursor: pointer; color: var(--tannengruen); margin: 0;">
<input type="checkbox" id="mpi-toggle-all" checked onchange="toggleAllMpiCheckboxes(this.checked)" style="width: 18px; height: 18px; accent-color: var(--tannengruen);">
<span>Alle auswählen / abwählen</span>
</label>
<span id="mpi-total-count" style="font-size: 0.85rem; color: #666; font-weight: 600;">0 Zutaten</span>
</div>
<!-- Scrollbare Zutaten-Liste -->
<div id="mpi-ingredients-container" style="max-height: 380px; overflow-y: auto; padding-right: 4px;">
<!-- Dynamisch via JS -->
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 18px; padding-top: 12px; border-top: 1.5px dashed rgba(34,34,34,0.15); flex-wrap: wrap; gap: 8px;">
<button type="button" onclick="closeMealPlanIngredientsModal()" class="cb-btn cb-btn-modal-cancel">
Abbrechen
</button>
<div style="display: flex; gap: 8px;">
<a href="/shopping-list" class="cb-btn cb-btn-outline" style="color: var(--schwarz); border-color: rgba(34,34,34,0.3); font-size: 0.9rem;">
<span>🛒</span> Zur Einkaufsliste
</a>
<button type="button" id="mpi-btn-transfer" onclick="transferMpiToShoppingList()" class="cb-btn cb-btn-creme" style="background: var(--tannengruen); color: var(--creme); font-weight: 700; font-size: 0.95rem; padding: 8px 18px;">
<span>➕</span> <span>Ausgewählte übertragen</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: SHUFFLE-GERICHT IN WOCHENPLAN -->
<!-- ========================================== -->
<div id="modal-shuffle-assign" class="cb-modal-backdrop no-print" onclick="onBackdropClick(event, 'modal-shuffle-assign')" style="z-index: 2250;">
<div class="cb-modal-card" style="max-width: 440px;">
<div class="cb-modal-header">
<h4 class="cb-modal-title"><span>📅</span> In Wochenplan einfügen</h4>
<button type="button" class="cb-modal-close" onclick="closeShuffleAssignModal()">&times;</button>
</div>
<div class="cb-modal-body">
<input type="hidden" id="sa-recipe-id">
<div style="margin-bottom: 14px; background: rgba(30,77,59,0.06); padding: 10px 14px; border-radius: 8px; border: 1.5px solid rgba(30,77,59,0.2);">
<span style="font-size: 0.8rem; color: #666; display: block; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700;">Rezept:</span>
<span id="sa-recipe-title" style="font-size: 1.15rem; font-weight: 700; color: var(--tannengruen); display: block; margin-top: 2px;"></span>
</div>
<div class="cb-form-group">
<label class="cb-label">Wochentag wählen:</label>
<select id="sa-day-select" class="cb-select">
<option value="0">Montag</option>
<option value="1">Dienstag</option>
<option value="2">Mittwoch</option>
<option value="3">Donnerstag</option>
<option value="4">Freitag</option>
<option value="5">Samstag</option>
<option value="6">Sonntag</option>
</select>
</div>
<div class="cb-form-group">
<label class="cb-label">Mahlzeit:</label>
<select id="sa-meal-type-select" class="cb-select">
<option value="mittag" selected>☀️ Mittag</option>
<option value="abend">🌙 Abend</option>
<option value="kaffee">☕ Kaffee & Kuchen</option>
<option value="frueh">🌅 Früh</option>
</select>
</div>
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; padding-top: 12px; border-top: 1px solid #ddd;">
<button type="button" onclick="closeShuffleAssignModal()" class="cb-btn cb-btn-modal-cancel">Abbrechen</button>
<button type="button" onclick="saveShuffleToMealPlan()" class="cb-btn cb-btn-modal-submit">💾 Eintragen</button>
</div>
</div>
</div>
</div>
<!-- ========================================== -->
<!-- MODAL 1: ESSENS-ZUFALLSGENERATOR (no-print)-->
<!-- ========================================== -->
@@ -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, '&quot;');
}
// 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>