/** * What To Cook? - Frontend Application */ // Category icons mapping const CATEGORY_ICONS = { "Reis": "🍚", "Nudeln": "🍝", "Kartoffeln": "🥔", "Teigig": "🍕", "asiatisch": "🥢", "Hausmannskost": "🥩", "Suppen": "🍲", "Eintöpfe": "🥘", "Süßspeisen": "🥞", "Fisch": "🐟" }; const CATEGORIES = [ "Reis", "Nudeln", "Kartoffeln", "Teigig", "asiatisch", "Hausmannskost", "Suppen", "Eintöpfe", "Süßspeisen", "Fisch" ]; const EFFORTS = ["gering", "mittel", "hoch"]; function getDishCategories(dish) { if (!dish) return []; if (Array.isArray(dish.categories) && dish.categories.length > 0) { return dish.categories; } if (dish.category) { return dish.category.split(",").map(c => c.trim()).filter(Boolean); } return []; } function renderCategoryBadges(dish) { const cats = getDishCategories(dish); if (cats.length === 0) return ""; return cats.map(cat => { const icon = CATEGORY_ICONS[cat] || "🍲"; return `${icon} ${cat}`; }).join(""); } // Application State const state = { activeTab: "generator", slots: [ { slot_number: 1, active: true, category: "", efforts: [], current_dish: null }, { slot_number: 2, active: true, category: "", efforts: [], current_dish: null }, { slot_number: 3, active: false, category: "", efforts: [], current_dish: null }, { slot_number: 4, active: false, category: "", efforts: [], current_dish: null }, { slot_number: 5, active: false, category: "", efforts: [], current_dish: null }, ], admin: { dishes: [], search: "", category: "alle", effort: "alle", deleteTargetId: null } }; // --- Initialization --- document.addEventListener("DOMContentLoaded", () => { initTabs(); renderSlots(); populateDropdowns(); initGeneratorEvents(); initAdminEvents(); loadAdminDishes(); }); // --- Tab Navigation --- function initTabs() { const btnGenerator = document.getElementById("tab-btn-generator"); const btnAdmin = document.getElementById("tab-btn-admin"); const panelGenerator = document.getElementById("tab-generator"); const panelAdmin = document.getElementById("tab-admin"); btnGenerator.addEventListener("click", () => switchTab("generator")); btnAdmin.addEventListener("click", () => switchTab("admin")); function switchTab(tabName) { state.activeTab = tabName; if (tabName === "generator") { btnGenerator.classList.add("active"); btnAdmin.classList.remove("active"); panelGenerator.classList.add("active"); panelAdmin.classList.remove("active"); panelAdmin.hidden = true; panelGenerator.hidden = false; } else { btnAdmin.classList.add("active"); btnGenerator.classList.remove("active"); panelAdmin.classList.add("active"); panelGenerator.classList.remove("active"); panelGenerator.hidden = true; panelAdmin.hidden = false; loadAdminDishes(); } } } // --- Populate Category Dropdowns --- function populateDropdowns() { const adminCatSelect = document.getElementById("admin-filter-category"); const modalCatGroup = document.getElementById("modal-category-group"); if (modalCatGroup) { modalCatGroup.innerHTML = ""; } CATEGORIES.forEach(cat => { const icon = CATEGORY_ICONS[cat] || "🍲"; // Admin filter if (adminCatSelect) { const optFilter = document.createElement("option"); optFilter.value = cat; optFilter.textContent = `${icon} ${cat}`; adminCatSelect.appendChild(optFilter); } // Modal checkbox pill if (modalCatGroup) { const label = document.createElement("label"); label.className = "category-checkbox-label"; label.innerHTML = ` ${icon} ${cat} `; modalCatGroup.appendChild(label); } }); } // --- Generator Slots Rendering --- function renderSlots() { const container = document.getElementById("slots-container"); container.innerHTML = ""; state.slots.forEach(slot => { const card = document.createElement("div"); card.className = `slot-card ${slot.active ? "active" : "inactive"}`; card.id = `slot-card-${slot.slot_number}`; card.innerHTML = `
${slot.slot_number} Gericht ${slot.slot_number}
`; container.appendChild(card); // Event: Toggle Active const toggleInput = card.querySelector(`#slot-toggle-${slot.slot_number}`); toggleInput.addEventListener("change", (e) => { slot.active = e.target.checked; renderSlots(); updateActiveCounter(); }); // Event: Category Change const catSelect = card.querySelector(`#slot-cat-${slot.slot_number}`); catSelect.addEventListener("change", (e) => { slot.category = e.target.value; }); // Event: Effort Multi-Select Chips const chips = card.querySelectorAll(`#effort-chips-${slot.slot_number} .effort-chip`); chips.forEach(chip => { chip.addEventListener("click", () => { const effortVal = chip.getAttribute("data-effort"); const idx = slot.efforts.indexOf(effortVal); if (idx > -1) { slot.efforts.splice(idx, 1); chip.classList.remove("selected"); } else { slot.efforts.push(effortVal); chip.classList.add("selected"); } }); }); }); updateActiveCounter(); } function updateActiveCounter() { const activeCount = state.slots.filter(s => s.active).length; const countEl = document.getElementById("active-slots-count"); if (countEl) countEl.textContent = activeCount; const btnHero = document.getElementById("btn-roll-all"); if (btnHero) { const btnText = btnHero.querySelector(".btn-text"); if (activeCount === 0) { btnText.textContent = "Bitte mindestens 1 Gericht aktivieren"; btnHero.disabled = true; } else if (activeCount === 1) { btnText.textContent = "1 Gericht auslosen"; btnHero.disabled = false; } else { btnText.textContent = `${activeCount} Gerichte auslosen`; btnHero.disabled = false; } } } // --- Generator Events (Roll Logic) --- function initGeneratorEvents() { const btnRollAll = document.getElementById("btn-roll-all"); const btnRerollAll = document.getElementById("btn-reroll-all"); const btnBack = document.getElementById("btn-back-to-config"); btnRollAll.addEventListener("click", () => executeRoll(false)); btnRerollAll.addEventListener("click", () => executeRoll(true)); btnBack.addEventListener("click", () => { window.scrollTo({ top: 0, behavior: "smooth" }); }); } async function executeRoll(isReroll = false) { const activeSlots = state.slots.filter(s => s.active); if (activeSlots.length === 0) { showToast("Bitte aktiviere mindestens 1 Gericht.", "error"); return; } const btnRollAll = document.getElementById("btn-roll-all"); const btnRerollAll = document.getElementById("btn-reroll-all"); btnRollAll.disabled = true; btnRerollAll.disabled = true; try { const payload = { slots: activeSlots.map(s => ({ slot_number: s.slot_number, category: s.category || null, efforts: s.efforts.length > 0 ? s.efforts : null, current_dish_id: isReroll && s.current_dish ? s.current_dish.id : null })) }; 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."); } // Update slots state with rolled dishes data.results.forEach(resItem => { const slot = state.slots.find(s => s.slot_number === resItem.slot_number); if (slot) { slot.current_dish = resItem.dish; } }); renderResults(data.results); showToast(isReroll ? "Alle Gerichte neu ausgelost!" : "Zufallsauswahl erfolgreich!", "success"); // Scroll smoothly to results const resultsSection = document.getElementById("results-section"); resultsSection.classList.remove("hidden"); resultsSection.scrollIntoView({ behavior: "smooth", block: "start" }); } catch (err) { showToast(err.message, "error"); } finally { btnRollAll.disabled = false; btnRerollAll.disabled = false; } } // Render Results Grid function renderResults(results) { const container = document.getElementById("results-list"); const countBadge = document.getElementById("results-count-badge"); container.innerHTML = ""; const validDishes = results.filter(r => r.dish !== null); countBadge.textContent = `${validDishes.length} ${validDishes.length === 1 ? "Gericht" : "Gerichte"}`; results.forEach(item => { const slot = state.slots.find(s => s.slot_number === item.slot_number); const dish = item.dish; const card = document.createElement("div"); card.className = "result-card"; card.id = `result-card-${item.slot_number}`; if (!dish) { card.innerHTML = `
Gericht ${item.slot_number}
⚠️ Keine passenden Gerichte gefunden

Für die gewählte Kombination (Kategorie / Aufwand) gibt es noch keine Einträge.

`; container.appendChild(card); return; } const effortLabel = dish.effort.charAt(0).toUpperCase() + dish.effort.slice(1); card.innerHTML = `
Gericht ${item.slot_number}
${dish.name}
${renderCategoryBadges(dish)} ${effortLabel}
`; container.appendChild(card); // Event: Single Reroll Button const btnRerollSingle = card.querySelector(".btn-reroll-single"); btnRerollSingle.addEventListener("click", () => rerollSingleSlot(item.slot_number, btnRerollSingle, card)); }); } // --- Single Slot Reroll Logic --- async function rerollSingleSlot(slotNumber, btnElement, cardElement) { const slot = state.slots.find(s => s.slot_number === slotNumber); if (!slot || !slot.current_dish) return; btnElement.classList.add("spinning"); cardElement.classList.add("rerolling"); // Collect other dishes currently on screen so we don't duplicate const otherDishIds = state.slots .filter(s => s.active && s.slot_number !== slotNumber && s.current_dish) .map(s => s.current_dish.id); try { const payload = { slot_number: slotNumber, category: slot.category || null, efforts: slot.efforts.length > 0 ? slot.efforts : null, current_dish_id: slot.current_dish.id, other_dish_ids: otherDishIds }; const res = await fetch("/api/roll-single", { 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 Neu-Würfeln."); if (data.status === "ok" && data.dish) { slot.current_dish = data.dish; const effortLabel = data.dish.effort.charAt(0).toUpperCase() + data.dish.effort.slice(1); // Smoothly update the card content const nameEl = cardElement.querySelector(`#dish-name-${slotNumber}`); if (nameEl) nameEl.textContent = data.dish.name; const tagsEl = cardElement.querySelector(".result-tags"); if (tagsEl) { tagsEl.innerHTML = ` ${renderCategoryBadges(data.dish)} ${effortLabel} `; } showToast(`Gericht ${slotNumber} neu gewürfelt: ${data.dish.name}`, "success"); } else { showToast("Kein anderes passendes Gericht in dieser Kategorie gefunden.", "error"); } } catch (err) { showToast(err.message, "error"); } finally { setTimeout(() => { btnElement.classList.remove("spinning"); cardElement.classList.remove("rerolling"); }, 450); } } // --- Admin Section & CRUD --- function initAdminEvents() { const searchInput = document.getElementById("admin-search-input"); const clearSearchBtn = document.getElementById("admin-search-clear"); const catFilter = document.getElementById("admin-filter-category"); const effortFilter = document.getElementById("admin-filter-effort"); searchInput.addEventListener("input", (e) => { state.admin.search = e.target.value.trim(); clearSearchBtn.classList.toggle("hidden", state.admin.search.length === 0); loadAdminDishes(); }); clearSearchBtn.addEventListener("click", () => { searchInput.value = ""; state.admin.search = ""; clearSearchBtn.classList.add("hidden"); loadAdminDishes(); }); catFilter.addEventListener("change", (e) => { state.admin.category = e.target.value; loadAdminDishes(); }); effortFilter.addEventListener("change", (e) => { state.admin.effort = e.target.value; loadAdminDishes(); }); // Modal Open/Close const dishModal = document.getElementById("dish-modal"); const btnOpenAdd = document.getElementById("btn-open-add-dish"); const btnCloseModal = document.getElementById("btn-close-modal"); const btnCancelModal = document.getElementById("btn-cancel-modal"); const dishForm = document.getElementById("dish-form"); btnOpenAdd.addEventListener("click", () => { openDishModal(); }); btnCloseModal.addEventListener("click", () => dishModal.close()); btnCancelModal.addEventListener("click", () => dishModal.close()); dishForm.addEventListener("submit", async (e) => { e.preventDefault(); await saveDishFromModal(); }); // Delete Modal const deleteModal = document.getElementById("delete-modal"); const btnCancelDelete = document.getElementById("btn-cancel-delete"); const btnConfirmDelete = document.getElementById("btn-confirm-delete"); btnCancelDelete.addEventListener("click", () => deleteModal.close()); btnConfirmDelete.addEventListener("click", async () => { if (state.admin.deleteTargetId) { await executeDeleteDish(state.admin.deleteTargetId); deleteModal.close(); } }); } async function loadAdminDishes() { const listContainer = document.getElementById("admin-dishes-list"); const statsSummary = document.getElementById("admin-stats-summary"); const countLabel = document.getElementById("dishes-count-label"); try { const params = new URLSearchParams(); if (state.admin.category && state.admin.category !== "alle") { params.append("category", state.admin.category); } if (state.admin.effort && state.admin.effort !== "alle") { params.append("effort", state.admin.effort); } if (state.admin.search) { params.append("search", state.admin.search); } const res = await fetch(`/api/dishes?${params.toString()}`); const dishes = await res.json(); state.admin.dishes = dishes; // Load overall stats const statsRes = await fetch("/api/stats"); const stats = await statsRes.json(); statsSummary.textContent = `${stats.total} Gerichte in der Datenbank`; countLabel.textContent = `${dishes.length} ${dishes.length === 1 ? "Gericht" : "Gerichte"} angezeigt`; if (dishes.length === 0) { listContainer.innerHTML = `
Keine Gerichte mit diesen Suchkriterien gefunden.
`; return; } listContainer.innerHTML = ""; dishes.forEach(dish => { const row = document.createElement("div"); row.className = "admin-dish-row"; const effortLabel = dish.effort.charAt(0).toUpperCase() + dish.effort.slice(1); row.innerHTML = `
${dish.name}
${renderCategoryBadges(dish)} ${effortLabel}
`; listContainer.appendChild(row); row.querySelector(".btn-edit").addEventListener("click", () => openDishModal(dish)); row.querySelector(".btn-delete").addEventListener("click", () => confirmDeleteDish(dish)); }); } catch (err) { listContainer.innerHTML = `
Fehler beim Laden: ${err.message}
`; } } function openDishModal(dish = null) { const modal = document.getElementById("dish-modal"); const modalTitle = document.getElementById("modal-title"); const formId = document.getElementById("dish-form-id"); const nameInput = document.getElementById("dish-name-input"); const catCheckboxes = document.querySelectorAll('input[name="modal-category"]'); if (dish) { modalTitle.textContent = "Gericht bearbeiten"; formId.value = dish.id; nameInput.value = dish.name; const cats = getDishCategories(dish); catCheckboxes.forEach(cb => { cb.checked = cats.includes(cb.value); }); const radio = document.querySelector(`input[name="modal-effort"][value="${dish.effort}"]`); if (radio) radio.checked = true; } else { modalTitle.textContent = "Neues Gericht anlegen"; formId.value = ""; nameInput.value = ""; catCheckboxes.forEach(cb => { cb.checked = false; }); const radio = document.querySelector(`input[name="modal-effort"][value="mittel"]`); if (radio) radio.checked = true; } modal.showModal(); } async function saveDishFromModal() { const modal = document.getElementById("dish-modal"); const formId = document.getElementById("dish-form-id").value; const name = document.getElementById("dish-name-input").value.trim(); const selectedCats = Array.from(document.querySelectorAll('input[name="modal-category"]:checked')).map(cb => cb.value); const effortRadio = document.querySelector('input[name="modal-effort"]:checked'); const effort = effortRadio ? effortRadio.value : "mittel"; if (!name) { showToast("Bitte gib einen Namen für das Gericht ein.", "error"); return; } if (selectedCats.length === 0) { showToast("Bitte wähle mindestens eine Kategorie aus.", "error"); return; } if (!effort) { showToast("Bitte wähle den Aufwand aus.", "error"); return; } const isEdit = !!formId; const url = isEdit ? `/api/dishes/${formId}` : "/api/dishes"; const method = isEdit ? "PUT" : "POST"; try { const res = await fetch(url, { method: method, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, categories: selectedCats, effort }) }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || "Fehler beim Speichern."); showToast(isEdit ? "Gericht aktualisiert!" : "Gericht erfolgreich hinzugefügt!", "success"); modal.close(); loadAdminDishes(); } catch (err) { showToast(err.message, "error"); } } function confirmDeleteDish(dish) { state.admin.deleteTargetId = dish.id; const deleteModal = document.getElementById("delete-modal"); const textEl = document.getElementById("delete-modal-text"); textEl.innerHTML = `Möchtest du "${dish.name}" wirklich löschen?`; deleteModal.showModal(); } async function executeDeleteDish(dishId) { try { const res = await fetch(`/api/dishes/${dishId}`, { method: "DELETE" }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || "Fehler beim Löschen."); showToast("Gericht wurde gelöscht.", "success"); loadAdminDishes(); } catch (err) { showToast(err.message, "error"); } } // --- Toast System --- function showToast(message, type = "info") { const container = document.getElementById("toast-container"); if (!container) return; const toast = document.createElement("div"); toast.className = `toast toast-${type}`; const icon = type === "success" ? "✅" : (type === "error" ? "⚠️" : "ℹ️"); toast.innerHTML = `${icon}${message}`; container.appendChild(toast); setTimeout(() => { toast.style.transition = "opacity 0.3s, transform 0.3s"; toast.style.opacity = "0"; toast.style.transform = "translateY(-10px)"; setTimeout(() => toast.remove(), 300); }, 3500); }