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:
+3
-1
@@ -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
@@ -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")
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user