Sort items alphabetically within categories, move live pill next to menu, fix category add button wrap, and fix meal plan ingredients import

This commit is contained in:
2026-10-15 22:11:02 +02:00
parent fef6956d62
commit 9665e245c2
3 changed files with 156 additions and 81 deletions
+3 -1
View File
@@ -1255,6 +1255,7 @@ def get_meal_plan_ingredients(year: int, week: int) -> List[Dict[str, Any]]:
"slot_label": slot_label,
"raw": raw_text,
"name": name,
"item_name": name,
"amount": amt,
"unit": unit
})
@@ -1264,7 +1265,8 @@ def get_meal_plan_ingredients(year: int, week: int) -> List[Dict[str, Any]]:
"recipe_id": recipe["id"],
"recipe_title": recipe["title"],
"slot_label": slot_label,
"items": items
"items": items,
"ingredients": items
})
return recipe_groups
+3 -2
View File
@@ -1069,8 +1069,9 @@ async def api_update_shopping_item(item_id: int, payload: ShoppingItemUpdate):
async def api_add_shopping_bulk(payload: ShoppingBulkCreate):
items_dicts = [it.model_dump() for it in payload.items]
count = add_shopping_items_bulk(items_dicts)
await shopping_ws_manager.broadcast({"action": "items_updated", "items": get_shopping_list()})
return {"status": "ok", "added_count": count}
current_items = get_shopping_list()
await shopping_ws_manager.broadcast({"action": "items_updated", "items": current_items})
return {"status": "ok", "added_count": count, "items": current_items}
@app.post("/api/shopping-list/{item_id}/toggle")
+150 -78
View File
@@ -402,64 +402,68 @@
<span class="cb-brand-icon">🛒</span>
<h1 class="cb-nav-title">Einkaufsliste</h1>
</a>
<span id="shop-online-pill" class="cb-online-pill" title="Synchronisations-Status">● Live</span>
<!-- Desktop Buttons (auf Bildschirmen > 768px) -->
<div class="cb-nav-actions desktop-nav-actions">
<button type="button" onclick="openCategoriesModal()" class="cb-nav-btn" title="Supermarkt-Bereiche & Laufweg anpassen">
<span>🏬</span> <span>Bereiche anpassen</span>
</button>
<button type="button" onclick="openMealPlanImportModal()" class="cb-nav-btn" title="Zutaten aus Wochenplan holen">
<span>📅</span> <span>Aus Wochenplan holen</span>
</button>
<button type="button" onclick="deleteCheckedItems()" class="cb-nav-btn" title="Erledigte Artikel löschen">
<span>🧹</span> <span>Erledigte löschen</span>
</button>
<button type="button" onclick="openDeleteStep1Modal()" class="cb-nav-btn cb-btn-danger" style="background:#b02a37; border-color:#b02a37; color:#fff;" title="Liste leeren">
<span>🗑️</span> <span>Liste leeren</span>
</button>
<a href="/" class="cb-nav-btn" title="Zur Startseite">
<span>🏠</span> <span>Startseite</span>
</a>
<a href="/cookbook" class="cb-nav-btn" title="Kochbuch & Rezepte">
<span>📖</span> <span>Kochbuch</span>
</a>
<a href="/logout" class="cb-nav-btn cb-nav-btn-logout" title="Abmelden">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
<!-- Rechte Navigation & Status -->
<div style="display: flex; align-items: center; gap: 8px; margin-left: auto;">
<span id="shop-online-pill" class="cb-online-pill" title="Synchronisations-Status">● Live</span>
<!-- Mobile 3-Punkte (Kebab) Menü (auf Bildschirmen <= 768px) -->
<div class="mobile-nav-wrapper">
<button type="button" class="cb-kebab-btn" onclick="toggleMobileMenu(event)" aria-label="Menü öffnen" title="Menü">
<span>⋮</span>
</button>
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
<button type="button" onclick="handleMobileMenuAction(openCategoriesModal)" class="cb-mobile-item">
<!-- Desktop Buttons (auf Bildschirmen > 768px) -->
<div class="cb-nav-actions desktop-nav-actions">
<button type="button" onclick="openCategoriesModal()" class="cb-nav-btn" title="Supermarkt-Bereiche & Laufweg anpassen">
<span>🏬</span> <span>Bereiche anpassen</span>
</button>
<button type="button" onclick="handleMobileMenuAction(openMealPlanImportModal)" class="cb-mobile-item">
<button type="button" onclick="openMealPlanImportModal()" class="cb-nav-btn" title="Zutaten aus Wochenplan holen">
<span>📅</span> <span>Aus Wochenplan holen</span>
</button>
<button type="button" onclick="handleMobileMenuAction(deleteCheckedItems)" class="cb-mobile-item">
<button type="button" onclick="deleteCheckedItems()" class="cb-nav-btn" title="Erledigte Artikel löschen">
<span>🧹</span> <span>Erledigte löschen</span>
</button>
<hr class="cb-mobile-divider">
<button type="button" onclick="handleMobileMenuAction(openDeleteStep1Modal)" class="cb-mobile-item" style="color: #b02a37; font-weight: 700;">
<button type="button" onclick="openDeleteStep1Modal()" class="cb-nav-btn cb-btn-danger" style="background:#b02a37; border-color:#b02a37; color:#fff;" title="Liste leeren">
<span>🗑️</span> <span>Liste leeren</span>
</button>
<hr class="cb-mobile-divider">
<a href="/" class="cb-mobile-item">
<a href="/" class="cb-nav-btn" title="Zur Startseite">
<span>🏠</span> <span>Startseite</span>
</a>
<a href="/cookbook" class="cb-mobile-item">
<a href="/cookbook" class="cb-nav-btn" title="Kochbuch & Rezepte">
<span>📖</span> <span>Kochbuch</span>
</a>
<hr class="cb-mobile-divider">
<a href="/logout" class="cb-mobile-item cb-mobile-logout">
<a href="/logout" class="cb-nav-btn cb-nav-btn-logout" title="Abmelden">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
<!-- Mobile 3-Punkte (Kebab) Menü (auf Bildschirmen <= 768px) -->
<div class="mobile-nav-wrapper">
<button type="button" class="cb-kebab-btn" onclick="toggleMobileMenu(event)" aria-label="Menü öffnen" title="Menü">
<span>⋮</span>
</button>
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
<button type="button" onclick="handleMobileMenuAction(openCategoriesModal)" class="cb-mobile-item">
<span>🏬</span> <span>Bereiche anpassen</span>
</button>
<button type="button" onclick="handleMobileMenuAction(openMealPlanImportModal)" class="cb-mobile-item">
<span>📅</span> <span>Aus Wochenplan holen</span>
</button>
<button type="button" onclick="handleMobileMenuAction(deleteCheckedItems)" class="cb-mobile-item">
<span>🧹</span> <span>Erledigte löschen</span>
</button>
<hr class="cb-mobile-divider">
<button type="button" onclick="handleMobileMenuAction(openDeleteStep1Modal)" class="cb-mobile-item" style="color: #b02a37; font-weight: 700;">
<span>🗑️</span> <span>Liste leeren</span>
</button>
<hr class="cb-mobile-divider">
<a href="/" class="cb-mobile-item">
<span>🏠</span> <span>Startseite</span>
</a>
<a href="/cookbook" class="cb-mobile-item">
<span>📖</span> <span>Kochbuch</span>
</a>
<hr class="cb-mobile-divider">
<a href="/logout" class="cb-mobile-item cb-mobile-logout">
<span>🚪</span> <span>Abmelden</span>
</a>
</div>
</div>
</div>
</header>
@@ -701,10 +705,12 @@
<div style="background: var(--shop-yellow); border: 1.5px solid var(--shop-schwarz); border-radius: 8px; padding: 12px;">
<h4 style="margin: 0 0 8px 0; font-size: 0.95rem;">Neuen Bereich anlegen</h4>
<form onsubmit="onAddCategory(event)" style="display: flex; gap: 8px;">
<input type="text" id="new-cat-icon" style="width: 44px; text-align: center; font-size: 1.2rem; border: 1.5px solid #888; border-radius: 6px;" value="🛒">
<input type="text" id="new-cat-name" placeholder="z. B. Drogerie & Haushalt" class="cb-input" style="flex: 1; padding: 8px 12px;" required>
<button type="submit" class="cb-btn cb-btn-green" style="padding: 8px 14px;">Hinzufügen</button>
<form onsubmit="onAddCategory(event)" style="display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; gap: 8px;">
<input type="text" id="new-cat-icon" style="width: 44px; text-align: center; font-size: 1.2rem; border: 1.5px solid #888; border-radius: 6px;" value="🛒" title="Symbol / Emoji">
<input type="text" id="new-cat-name" placeholder="z. B. Drogerie & Haushalt" class="cb-input" style="flex: 1; padding: 8px 12px;" required>
</div>
<button type="submit" class="cb-btn cb-btn-green" style="width: 100%; justify-content: center; padding: 9px 14px; font-weight: 700;">+ Bereich hinzufügen</button>
</form>
</div>
@@ -1008,6 +1014,8 @@
let html = '';
groupNames.forEach(catName => {
const group = groups[catName];
group.open.sort((a, b) => (a.item_name || '').localeCompare(b.item_name || '', 'de', { sensitivity: 'base' }));
group.checked.sort((a, b) => (a.item_name || '').localeCompare(b.item_name || '', 'de', { sensitivity: 'base' }));
const allInGroup = [...group.open, ...group.checked];
if (allInGroup.length === 0) return;
@@ -1296,61 +1304,97 @@
// ==========================================
// MODAL: WOCHENPLAN ZUTATEN IMPORTIEREN
// ==========================================
async function openMealPlanImportModal() {
let mealPlanYear = null;
let mealPlanWeek = null;
async function openMealPlanImportModal(year, week) {
openModal('modal-import-mealplan');
const body = document.getElementById('mealplan-import-body');
const confirmBtn = document.getElementById('btn-confirm-import-mealplan');
if (confirmBtn) confirmBtn.disabled = true;
if (confirmBtn) {
confirmBtn.disabled = true;
confirmBtn.textContent = 'Ausgewählte übernehmen';
}
body.innerHTML = `
<div style="text-align: center; padding: 25px; color: #666;">
<span>⏳ Zutaten des aktuellen Wochenplans werden geladen...</span>
<span>⏳ Zutaten des Wochenplans werden geladen...</span>
</div>
`;
try {
const res = await fetch('/api/mealplan/ingredients');
let url = '/api/mealplan/ingredients';
if (year && week) {
url += `?year=${encodeURIComponent(year)}&week=${encodeURIComponent(week)}`;
}
const res = await fetch(url);
if (!res.ok) throw new Error('Fehler beim Laden');
const data = await res.json();
mealPlanYear = data.year;
mealPlanWeek = data.week;
const groups = data.groups || [];
let navHeader = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; gap: 8px; flex-wrap: wrap;">
<div style="display: flex; align-items: center; gap: 6px;">
<button type="button" class="cb-btn cb-btn-secondary" style="padding: 3px 9px; font-size: 0.85rem;" onclick="changeMealPlanImportWeek(-1)" title="Vorherige Woche">◀</button>
<span style="font-weight: 700; font-size: 0.95rem;">KW ${data.week} / ${data.year}</span>
<button type="button" class="cb-btn cb-btn-secondary" style="padding: 3px 9px; font-size: 0.85rem;" onclick="changeMealPlanImportWeek(1)" title="Nächste Woche">▶</button>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<button type="button" onclick="toggleAllMealPlanChecks(true)" style="background: none; border: none; color: var(--shop-green); font-size: 0.85rem; font-weight: 700; cursor: pointer; text-decoration: underline;">
Alle
</button>
<span style="color: #ccc;">|</span>
<button type="button" onclick="toggleAllMealPlanChecks(false)" style="background: none; border: none; color: #888; font-size: 0.85rem; font-weight: 600; cursor: pointer; text-decoration: underline;">
Keine
</button>
</div>
</div>
`;
if (groups.length === 0) {
body.innerHTML = `
body.innerHTML = navHeader + `
<div style="text-align: center; padding: 25px; color: #666;">
<p style="font-size: 1.1rem; margin: 0 0 10px 0;">Keine Rezepte im Wochenplan gefunden.</p>
<p style="font-size: 0.9rem; margin: 0;">Plane zuerst Gerichte im Kochbuch-Wochenmenüplan ein!</p>
<span style="font-size: 2.2rem; display: block; margin-bottom: 8px;">🍽️</span>
<p style="font-size: 1.05rem; margin: 0 0 6px 0; font-weight: 700; color: var(--shop-green);">Keine Rezepte in dieser Woche geplant</p>
<p style="font-size: 0.88rem; margin: 0; color: #777;">Blättere mit ◀ oder ▶ zu einer anderen Woche oder trage Gerichte im Kochbuch-Wochenmenüplan ein.</p>
</div>
`;
return;
}
let html = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 700; font-size: 0.92rem;">KW ${data.week} / ${data.year}</span>
<button type="button" onclick="toggleAllMealPlanChecks(true)" style="background: none; border: none; color: var(--shop-green); font-size: 0.85rem; font-weight: 700; cursor: pointer; text-decoration: underline;">
Alle auswählen
</button>
</div>
<div style="max-height: 50vh; overflow-y: auto; display: flex; flex-direction: column; gap: 14px;">
let totalItems = 0;
let html = navHeader + `
<div style="max-height: 52vh; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; padding-right: 4px;">
`;
groups.forEach((g, gIdx) => {
const items = g.items || g.ingredients || [];
totalItems += items.length;
html += `
<div style="background: rgba(30, 77, 59, 0.05); border: 1.5px solid rgba(30, 77, 59, 0.15); border-radius: 8px; padding: 10px;">
<div style="font-family: var(--font-hand); font-size: 1.25rem; font-weight: 700; color: var(--shop-green); margin-bottom: 6px;">
🍳 ${escapeHtml(g.recipe_title)}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div style="font-family: var(--font-hand); font-size: 1.25rem; font-weight: 700; color: var(--shop-green);">
🍳 ${escapeHtml(g.recipe_title)}
</div>
${g.slot_label ? `<span style="font-size: 0.75rem; background: rgba(30, 77, 59, 0.12); color: var(--shop-green); padding: 2px 7px; border-radius: 6px; font-weight: 600;">${escapeHtml(g.slot_label)}</span>` : ''}
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
${g.ingredients.map((ing, iIdx) => `
<label style="display: flex; align-items: center; gap: 8px; font-size: 0.92rem; cursor: pointer; padding: 3px 0;">
<input type="checkbox" class="mealplan-ing-checkbox" checked
data-name="${escapeAttr(ing.item_name)}"
data-amount="${escapeAttr(ing.amount || '')}"
data-unit="${escapeAttr(ing.unit || '')}"
onchange="updateMealPlanConfirmBtn()">
<span><strong>${escapeHtml(formatAmount(ing.amount, ing.unit))}</strong> ${escapeHtml(ing.item_name)}</span>
</label>
`).join('')}
${items.map((ing, iIdx) => {
const ingName = ing.name || ing.item_name || ing.raw || '';
const amtStr = formatAmount(ing.amount, ing.unit);
return `
<label style="display: flex; align-items: center; gap: 8px; font-size: 0.92rem; cursor: pointer; padding: 3px 0;">
<input type="checkbox" class="mealplan-ing-checkbox" checked
data-name="${escapeAttr(ingName)}"
data-amount="${escapeAttr(ing.amount || '')}"
data-unit="${escapeAttr(ing.unit || '')}"
onchange="updateMealPlanConfirmBtn()">
<span>${amtStr ? `<strong>${escapeHtml(amtStr)}</strong> ` : ''}${escapeHtml(ingName)}</span>
</label>
`;
}).join('')}
</div>
</div>
`;
@@ -1358,25 +1402,46 @@
html += `</div>`;
body.innerHTML = html;
if (confirmBtn) confirmBtn.disabled = false;
if (confirmBtn) {
confirmBtn.disabled = totalItems === 0;
confirmBtn.textContent = `Ausgewählte übernehmen (${totalItems})`;
}
} catch (err) {
console.error("Fehler beim Laden des Wochenplans:", err);
body.innerHTML = `
<div style="text-align: center; padding: 20px; color: #b00;">
<div style="text-align: center; padding: 20px; color: #b02a37;">
Fehler beim Laden des Wochenplans. Bitte stelle sicher, dass du online bist.
</div>
`;
}
}
function changeMealPlanImportWeek(delta) {
if (!mealPlanYear || !mealPlanWeek) return;
let w = mealPlanWeek + delta;
let y = mealPlanYear;
if (w < 1) {
y -= 1;
w = 52;
} else if (w > 52) {
y += 1;
w = 1;
}
openMealPlanImportModal(y, w);
}
function toggleAllMealPlanChecks(val) {
document.querySelectorAll('.mealplan-ing-checkbox').forEach(cb => cb.checked = val);
updateMealPlanConfirmBtn();
}
function updateMealPlanConfirmBtn() {
const anyChecked = Array.from(document.querySelectorAll('.mealplan-ing-checkbox')).some(cb => cb.checked);
const checkedBoxes = Array.from(document.querySelectorAll('.mealplan-ing-checkbox:checked'));
const btn = document.getElementById('btn-confirm-import-mealplan');
if (btn) btn.disabled = !anyChecked;
if (btn) {
btn.disabled = checkedBoxes.length === 0;
btn.textContent = `Ausgewählte übernehmen (${checkedBoxes.length})`;
}
}
async function confirmImportMealPlanIngredients() {
@@ -1402,13 +1467,20 @@
});
if (res.ok) {
const d = await res.json();
allItems = d.items || [];
if (d.items && Array.isArray(d.items)) {
allItems = d.items;
} else {
await refreshDataFromServer();
}
localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems));
renderCategoryFilterChips();
renderShoppingList();
showToast(`${itemsToAdd.length} Zutaten übernommen`);
showToast(`✓ ${itemsToAdd.length} Zutaten übernommen`);
} else {
showToast('Fehler beim Übernehmen');
}
} catch (err) {
console.error("Fehler beim Importieren der Zutaten:", err);
showToast('Fehler beim Übernehmen');
}
}