From 5290d73502b6ac49e72025792071f62cb836eb53 Mon Sep 17 00:00:00 2001 From: kobold-ai Date: Thu, 29 Oct 2026 20:11:18 +0100 Subject: [PATCH] Add cookbook recipe import to shopping list with deduplication and quantity merging --- app/templates/recipe_detail.html | 6 + app/templates/shopping_list.html | 446 ++++++++++++++++++++++++++++++- 2 files changed, 448 insertions(+), 4 deletions(-) diff --git a/app/templates/recipe_detail.html b/app/templates/recipe_detail.html index 7b9d652..d89dd7a 100755 --- a/app/templates/recipe_detail.html +++ b/app/templates/recipe_detail.html @@ -24,6 +24,9 @@ Aus {% if not is_shared %} + + 🛒 Auf Einkaufsliste + 📖 Alle Rezepte @@ -65,6 +68,9 @@
{% if not is_shared %} + + 🛒 Auf Einkaufsliste + diff --git a/app/templates/shopping_list.html b/app/templates/shopping_list.html index 1b7c009..2ee0bf0 100644 --- a/app/templates/shopping_list.html +++ b/app/templates/shopping_list.html @@ -541,6 +541,9 @@ + @@ -573,6 +576,9 @@ + @@ -616,10 +622,15 @@ @@ -816,6 +827,31 @@ + + + + + @@ -1813,6 +1849,398 @@ } } + // ========================================== + // MODAL: ZUTATEN AUS KOCHBUCH HOLEN + // ========================================== + let allCookbookRecipes = null; + let selectedCookbookRecipes = []; + + const CHAPTER_NAMES = { + 1: "Suppen & Eintöpfe", + 2: "Traditionell & Basics", + 3: "Pasta & Nudelgerichte", + 4: "Kartoffeln", + 5: "Reis & Getreide", + 6: "Teig & Gebackenes", + 7: "Asiatische Küche", + 8: "Süßspeisen", + 9: "Desserts", + 10: "Kuchen & Gebäck" + }; + + function cleanIngredientName(name) { + if (!name) return ''; + let str = name.trim().replace(/^[ \-\*\s]+/, ''); + str = str.replace(/\s*\([^)]*\)/g, ''); + if (str.includes(',')) { + const parts = str.split(','); + if (parts[1].trim().split(/\s+/).length <= 4) { + str = parts[0]; + } + } + str = str.replace(/[\s.,;:]+$/, '').replace(/\s+/g, ' ').trim(); + if (str && str[0] === str[0].toLowerCase()) { + str = str[0].toUpperCase() + str.slice(1); + } + return str; + } + + function getNormalizedIngredientKey(name) { + if (!name) return ""; + let str = name.toLowerCase().replace(/^[ \-\*\s]+/, "").replace(/\s*\([^)]*\)/g, ""); + if (str.includes(",")) str = str.split(",")[0]; + str = str.replace(/[\s.,;:]+$/, "").replace(/\s+/g, " ").trim(); + if (str.endsWith("en") && str.length > 5) str = str.slice(0, -2); + else if (str.endsWith("n") && str.length > 4 && !str.endsWith("in")) str = str.slice(0, -1); + return str; + } + + function findMatchingOpenShoppingItem(name) { + const k = getNormalizedIngredientKey(name); + if (!k) return null; + return allItems.find(it => !it.checked && getNormalizedIngredientKey(it.item_name) === k) || null; + } + + async function openCookbookImportModal(preselectedRecipeId = null) { + openModal('modal-import-cookbook'); + const body = document.getElementById('cookbook-import-body'); + const confirmBtn = document.getElementById('btn-confirm-import-cookbook'); + if (confirmBtn) { + confirmBtn.disabled = true; + confirmBtn.textContent = 'Ausgewählte übernehmen'; + } + + if (!allCookbookRecipes) { + body.innerHTML = ` +
+
+
Rezepte werden geladen...
+
+ `; + try { + const res = await fetch('/api/recipes'); + if (!res.ok) throw new Error('Fehler beim Laden der Rezepte'); + allCookbookRecipes = await res.json(); + } catch (err) { + console.error('Fehler beim Laden der Rezepte:', err); + body.innerHTML = ` +
+ Fehler beim Laden der Rezepte. Bitte stelle sicher, dass du online bist. +
+ `; + return; + } + } + + if (preselectedRecipeId) { + selectedCookbookRecipes = []; + } + + renderCookbookImportUI(); + + if (preselectedRecipeId) { + await addCookbookRecipeToSelection(preselectedRecipeId); + } + } + + function renderCookbookImportUI() { + const body = document.getElementById('cookbook-import-body'); + if (!body) return; + + const recipesByChapter = {}; + allCookbookRecipes.forEach(r => { + const ch = r.chapter_number || 1; + if (!recipesByChapter[ch]) recipesByChapter[ch] = []; + recipesByChapter[ch].push(r); + }); + + const sortedChapters = Object.keys(recipesByChapter).map(Number).sort((a,b) => a - b); + + let optionsHtml = ''; + sortedChapters.forEach(ch => { + const chName = CHAPTER_NAMES[ch] || `Kapitel ${ch}`; + optionsHtml += ``; + recipesByChapter[ch].forEach(r => { + const sub = r.subtitle ? ` (${r.subtitle})` : ''; + optionsHtml += ``; + }); + optionsHtml += ``; + }); + + body.innerHTML = ` +
+
+ + ${allCookbookRecipes.length} Rezepte +
+
+ + + +
+
+ +
+ ${renderSelectedCookbookRecipeCards()} +
+ `; + + updateCookbookConfirmBtn(); + } + + function renderSelectedCookbookRecipeCards() { + if (selectedCookbookRecipes.length === 0) { + return ` +
+ 📖 +

Noch kein Rezept ausgewählt

+

+ Wähle oben ein Rezept aus, um dessen Zutaten auf deine Einkaufsliste zu setzen. Mengen von bereits vorhandenen Zutaten werden automatisch addiert – ohne Dopplungen! +

+
+ `; + } + + return selectedCookbookRecipes.map(r => { + const ingredients = r.parsed_ingredients || []; + const chName = CHAPTER_NAMES[r.chapter_number] || `Kapitel ${r.chapter_number || 1}`; + + let itemsHtml = ''; + if (ingredients.length === 0) { + itemsHtml = `
Keine Zutaten für dieses Rezept hinterlegt.
`; + } else { + itemsHtml = ingredients.map(ing => { + if (ing.is_header) { + return `
${escapeHtml(ing.raw)}
`; + } + + const cleanName = cleanIngredientName(ing.name || ing.raw || ''); + if (!cleanName) return ''; + + const amtStr = formatAmount(ing.amount_formatted || ing.amount_str || ing.amount, ing.unit); + const match = findMatchingOpenShoppingItem(cleanName); + + return ` + + `; + }).join(''); + } + + return ` +
+
+
+
+ 🍳 ${escapeHtml(r.title)} +
+ ${r.subtitle ? `
${escapeHtml(r.subtitle)}
` : ''} +
+
+ ${escapeHtml(chName)} + +
+
+ +
+ + | + +
+ +
+ ${itemsHtml} +
+
+ `; + }).join(''); + } + + async function addCookbookRecipeToSelection(recipeId) { + recipeId = parseInt(recipeId, 10); + if (!recipeId) return; + + if (selectedCookbookRecipes.some(r => r.id === recipeId)) { + showToast('Rezept bereits in der Auswahl'); + return; + } + + try { + const res = await fetch(`/api/recipes/${recipeId}`); + if (!res.ok) throw new Error('Rezept nicht gefunden'); + const recipeData = await res.json(); + selectedCookbookRecipes.push(recipeData); + + const container = document.getElementById('cookbook-selected-recipes-container'); + if (container) { + container.innerHTML = renderSelectedCookbookRecipeCards(); + } + updateCookbookConfirmBtn(); + } catch (err) { + console.error('Fehler beim Laden des Rezepts:', err); + showToast('Fehler beim Laden des Rezepts'); + } + } + + function removeCookbookRecipeFromSelection(recipeId) { + selectedCookbookRecipes = selectedCookbookRecipes.filter(r => r.id !== recipeId); + const container = document.getElementById('cookbook-selected-recipes-container'); + if (container) { + container.innerHTML = renderSelectedCookbookRecipeCards(); + } + updateCookbookConfirmBtn(); + } + + function toggleCookbookRecipeChecks(recipeId, val) { + const cbs = document.querySelectorAll(`.cookbook-ing-checkbox[data-recipe-id="${recipeId}"]`); + cbs.forEach(cb => cb.checked = val); + updateCookbookConfirmBtn(); + } + + function onSelectCookbookDropdown(recipeId) { + if (!recipeId) return; + addCookbookRecipeToSelection(recipeId); + const sel = document.getElementById('cookbook-recipe-select'); + if (sel) sel.value = ''; + } + + function onFilterCookbookSearch(query) { + const resultsDiv = document.getElementById('cookbook-search-dropdown-results'); + if (!resultsDiv || !allCookbookRecipes) return; + + const q = (query || '').toLowerCase().trim(); + if (!q) { + resultsDiv.style.display = 'none'; + resultsDiv.innerHTML = ''; + return; + } + + const matches = allCookbookRecipes.filter(r => { + const titleMatch = (r.title || '').toLowerCase().includes(q); + const subMatch = (r.subtitle || '').toLowerCase().includes(q); + const ingMatch = (r.ingredients || '').toLowerCase().includes(q); + return titleMatch || subMatch || ingMatch; + }).slice(0, 10); + + if (matches.length === 0) { + resultsDiv.innerHTML = `
Kein passendes Rezept gefunden
`; + resultsDiv.style.display = 'block'; + return; + } + + resultsDiv.innerHTML = matches.map(r => { + const chName = CHAPTER_NAMES[r.chapter_number] || `Kapitel ${r.chapter_number || 1}`; + return ` +
+
+ ${escapeHtml(r.title)} + ${r.subtitle ? `${escapeHtml(r.subtitle)}` : ''} +
+ ${escapeHtml(chName)} +
+ `; + }).join(''); + resultsDiv.style.display = 'block'; + } + + function selectSearchRecipeMatch(recipeId) { + addCookbookRecipeToSelection(recipeId); + const searchInput = document.getElementById('cookbook-search-input'); + if (searchInput) searchInput.value = ''; + const resultsDiv = document.getElementById('cookbook-search-dropdown-results'); + if (resultsDiv) { + resultsDiv.style.display = 'none'; + resultsDiv.innerHTML = ''; + } + } + + function updateCookbookConfirmBtn() { + const checkedBoxes = Array.from(document.querySelectorAll('.cookbook-ing-checkbox:checked')); + const btn = document.getElementById('btn-confirm-import-cookbook'); + if (btn) { + btn.disabled = checkedBoxes.length === 0; + btn.textContent = `Ausgewählte übernehmen (${checkedBoxes.length})`; + } + } + + async function confirmImportCookbookIngredients() { + const cbs = Array.from(document.querySelectorAll('.cookbook-ing-checkbox:checked')); + if (cbs.length === 0) return; + + const confirmBtn = document.getElementById('btn-confirm-import-cookbook'); + if (confirmBtn) { + confirmBtn.disabled = true; + confirmBtn.innerHTML = ' Wird übernommen...'; + } + + const itemsToAdd = cbs.map(cb => ({ + item_name: cb.dataset.name, + amount: cb.dataset.amount || '', + unit: cb.dataset.unit || '', + source: cb.dataset.source || '', + category: null, + note: '' + })); + + try { + const res = await fetch('/api/shopping-list/bulk', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ items: itemsToAdd }) + }); + + if (res.ok) { + const d = await res.json(); + if (d.items && Array.isArray(d.items)) { + allItems = d.items; + } else { + await refreshDataFromServer(); + } + saveLocalCache(allItems); + closeModal('modal-import-cookbook'); + renderCategoryFilterChips(); + renderShoppingList(); + showToast(`✓ ${itemsToAdd.length} Zutaten übernommen (Mengen zusammengeführt)`); + } else { + showToast('Fehler beim Übernehmen'); + if (confirmBtn) { + confirmBtn.disabled = false; + confirmBtn.textContent = 'Ausgewählte übernehmen'; + } + } + } catch (err) { + console.error('Fehler beim Importieren aus dem Kochbuch:', err); + showToast('Fehler beim Übernehmen'); + if (confirmBtn) { + confirmBtn.disabled = false; + confirmBtn.textContent = 'Ausgewählte übernehmen'; + } + } + } + // ========================================== // ========================================== // MODAL: SUPERMARKT-BEREICHE VERWALTUNG @@ -2108,6 +2536,16 @@ renderCategoryFilterChips(); renderShoppingList(); initWebSocket(); + + // Prüfen, ob ein Rezept per URL-Parameter übergeben wurde + const urlParams = new URLSearchParams(window.location.search); + const impRecipeId = urlParams.get('import_recipe_id'); + if (impRecipeId) { + window.history.replaceState({}, document.title, window.location.pathname); + setTimeout(() => { + openCookbookImportModal(parseInt(impRecipeId, 10)); + }, 120); + } });