Add ingredient aggregation, smart category mapping, and toggle to hide checked items

This commit is contained in:
2026-10-19 21:44:00 +02:00
parent f6c1f4e232
commit 3709840d7a
4 changed files with 738 additions and 84 deletions
+74 -8
View File
@@ -48,6 +48,12 @@
width: 100%;
background: var(--shop-green);
}
.cb-nav-btn.is-active {
background: rgba(255, 255, 255, 0.28) !important;
border-color: rgba(255, 255, 255, 0.6) !important;
color: #FFFFFF !important;
font-weight: 700;
}
/* Live Online Badge in Navbar */
.cb-online-pill {
@@ -435,6 +441,9 @@
<!-- Desktop Buttons (auf Bildschirmen > 768px) -->
<div class="cb-nav-actions desktop-nav-actions">
<button type="button" id="btn-toggle-hide-checked" onclick="toggleHideChecked()" class="cb-nav-btn" title="Erledigte Artikel ausblenden / einblenden">
<span id="hide-checked-icon">👁️</span> <span id="hide-checked-text">Erledigte ausblenden</span>
</button>
<button type="button" onclick="openCategoriesModal()" class="cb-nav-btn" title="Supermarkt-Bereiche & Laufweg anpassen">
<span>🏬</span> <span>Bereiche anpassen</span>
</button>
@@ -464,6 +473,9 @@
<span>⋮</span>
</button>
<div id="cb-mobile-menu" class="cb-mobile-dropdown">
<button type="button" onclick="handleMobileMenuAction(toggleHideChecked)" class="cb-mobile-item">
<span id="hide-checked-mobile-icon">👁️</span> <span id="hide-checked-mobile-text">Erledigte ausblenden</span>
</button>
<button type="button" onclick="handleMobileMenuAction(openCategoriesModal)" class="cb-mobile-item">
<span>🏬</span> <span>Bereiche anpassen</span>
</button>
@@ -760,6 +772,7 @@
let allItems = [];
let allCategories = [];
let currentFilterCategory = null;
let hideChecked = localStorage.getItem('wtc_hide_checked') === 'true';
let ws = null;
let reconnectTimeout = null;
let pingInterval = null;
@@ -975,6 +988,39 @@
}
}
// ==========================================
// TOGGLE: ERLEDIGTE AUSBLENDEN / EINBLENDEN
// ==========================================
function toggleHideChecked() {
hideChecked = !hideChecked;
localStorage.setItem('wtc_hide_checked', hideChecked ? 'true' : 'false');
updateHideCheckedUI();
renderCategoryFilterChips();
renderShoppingList();
}
function updateHideCheckedUI() {
const desktopBtn = document.getElementById('btn-toggle-hide-checked');
const desktopIcon = document.getElementById('hide-checked-icon');
const desktopText = document.getElementById('hide-checked-text');
const mobileIcon = document.getElementById('hide-checked-mobile-icon');
const mobileText = document.getElementById('hide-checked-mobile-text');
if (hideChecked) {
if (desktopBtn) desktopBtn.classList.add('is-active');
if (desktopIcon) desktopIcon.textContent = '👁️🗨️';
if (desktopText) desktopText.textContent = 'Erledigte anzeigen';
if (mobileIcon) mobileIcon.textContent = '👁️🗨️';
if (mobileText) mobileText.textContent = 'Erledigte anzeigen';
} else {
if (desktopBtn) desktopBtn.classList.remove('is-active');
if (desktopIcon) desktopIcon.textContent = '👁️';
if (desktopText) desktopText.textContent = 'Erledigte ausblenden';
if (mobileIcon) mobileIcon.textContent = '👁️';
if (mobileText) mobileText.textContent = 'Erledigte ausblenden';
}
}
// ==========================================
// RENDERING: KATEGORIEN-FILTERLEISTE
// ==========================================
@@ -982,7 +1028,8 @@
const container = document.getElementById('shop-category-chips');
if (!container) return;
const totalItems = allItems.length;
const relevantItems = hideChecked ? allItems.filter(it => !it.checked) : allItems;
const totalItems = relevantItems.length;
let html = `
<button type="button" class="cb-chip ${currentFilterCategory === null ? 'active' : ''}" onclick="selectFilterCategory(null)">
Alle (${totalItems})
@@ -990,7 +1037,7 @@
`;
allCategories.forEach(cat => {
const count = allItems.filter(it => it.category === cat.name).length;
const count = relevantItems.filter(it => it.category === cat.name).length;
if (count > 0 || currentFilterCategory === cat.name) {
const isAct = currentFilterCategory === cat.name;
html += `
@@ -1002,7 +1049,7 @@
});
// 'Sonstiges' anzeigen falls vorhanden
const uncategorizedCount = allItems.filter(it => !it.category || it.category === 'Sonstiges').length;
const uncategorizedCount = relevantItems.filter(it => !it.category || it.category === 'Sonstiges').length;
if (uncategorizedCount > 0 && !allCategories.some(c => c.name === 'Sonstiges')) {
const isAct = currentFilterCategory === 'Sonstiges';
html += `
@@ -1034,10 +1081,25 @@
if (currentFilterCategory !== null) {
filtered = filtered.filter(it => (it.category || 'Sonstiges') === currentFilterCategory);
}
if (hideChecked) {
filtered = filtered.filter(it => !it.checked);
}
if (filtered.length === 0) {
container.innerHTML = '';
if (emptyState) emptyState.style.display = 'block';
if (emptyState) {
emptyState.style.display = 'block';
const totalOpen = allItems.filter(it => !it.checked).length;
const titleEl = emptyState.querySelector('.shop-empty-title');
const descEl = emptyState.querySelector('.shop-empty-desc');
if (hideChecked && allItems.length > 0 && totalOpen === 0) {
if (titleEl) titleEl.textContent = 'Alle offenen Artikel erledigt!';
if (descEl) descEl.innerHTML = 'Alle Artikel auf deiner Liste sind abgehakt und ausgeblendet. Klicke auf <strong>Erledigte anzeigen</strong>, um sie wieder einzublenden.';
} else {
if (titleEl) titleEl.textContent = 'Alles eingekauft!';
if (descEl) descEl.innerHTML = 'Deine Einkaufsliste ist leer. Tippe unten rechts auf das <strong>+</strong>, um einen neuen Eintrag zu notieren, oder übernimm Zutaten direkt aus dem Wochenplan.';
}
}
return;
} else {
if (emptyState) emptyState.style.display = 'none';
@@ -1077,10 +1139,11 @@
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;
const allInGroup = hideChecked ? group.open : [...group.open, ...group.checked];
if (allInGroup.length === 0) return; // Kategorie komplett ausblenden, wenn keine Einträge!
const catObj = allCategories.find(c => c.name === catName) || { name: catName, icon: '🛒' };
const countBadge = hideChecked ? `${group.open.length} offen` : `${group.open.length} offen / ${allInGroup.length}`;
html += `
<section class="shop-cat-section">
@@ -1088,7 +1151,7 @@
<h2 class="shop-cat-title">
<span>${catObj.icon || '🛒'}</span> <span>${escapeHtml(catName)}</span>
</h2>
<span class="shop-cat-count">${group.open.length} offen / ${allInGroup.length}</span>
<span class="shop-cat-count">${countBadge}</span>
</div>
<ul class="shop-items-list">
${allInGroup.map(item => renderItemRow(item)).join('')}
@@ -1147,6 +1210,7 @@
// Optimistic UI Update
item.checked = !item.checked;
saveLocalCache(allItems);
renderCategoryFilterChips();
renderShoppingList();
try {
@@ -1453,6 +1517,7 @@
data-name="${escapeAttr(ingName)}"
data-amount="${escapeAttr(ing.amount || '')}"
data-unit="${escapeAttr(ing.unit || '')}"
data-source="${escapeAttr(g.recipe_title || '')}"
onchange="updateMealPlanConfirmBtn()">
<span>${amtStr ? `<strong>${escapeHtml(amtStr)}</strong> ` : ''}${escapeHtml(ingName)}</span>
</label>
@@ -1521,7 +1586,7 @@
item_name: cb.dataset.name,
amount: cb.dataset.amount || '',
unit: cb.dataset.unit || '',
source: '',
source: cb.dataset.source || '',
category: null,
note: ''
}));
@@ -1745,6 +1810,7 @@
}
document.addEventListener('DOMContentLoaded', () => {
updateHideCheckedUI();
renderCategoryFilterChips();
renderShoppingList();
initWebSocket();