diff --git a/app/database.py b/app/database.py index ca3e74a..7f16ccc 100755 --- a/app/database.py +++ b/app/database.py @@ -1182,6 +1182,18 @@ def toggle_shopping_item(item_id: int) -> Optional[Dict[str, Any]]: return row +def set_shopping_item_checked(item_id: int, checked: bool) -> Optional[Dict[str, Any]]: + """Setzt den Erledigt-Status eines Eintrags explizit auf True oder False.""" + val = 1 if checked else 0 + query = "UPDATE shopping_list SET checked = ? WHERE id = ?" + execute_query(query, (val, item_id), fetch="none", commit=True) + row = execute_query("SELECT * FROM shopping_list WHERE id = ?", (item_id,), fetch="one") + if row: + row["checked"] = bool(row.get("checked")) + row["display_source"] = clean_source_label(row.get("source", "")) + return row + + def delete_shopping_item(item_id: int) -> bool: """Löscht einen einzelnen Eintrag aus der Einkaufsliste.""" query = "DELETE FROM shopping_list WHERE id = ?" diff --git a/app/main.py b/app/main.py index 9ca0235..152aff0 100755 --- a/app/main.py +++ b/app/main.py @@ -47,6 +47,7 @@ from .database import ( add_shopping_item, add_shopping_items_bulk, toggle_shopping_item, + set_shopping_item_checked, delete_shopping_item, delete_checked_shopping_items, clear_shopping_list, @@ -1075,8 +1076,11 @@ async def api_add_shopping_bulk(payload: ShoppingBulkCreate): @app.post("/api/shopping-list/{item_id}/toggle") -async def api_toggle_shopping_item(item_id: int): - updated = toggle_shopping_item(item_id) +async def api_toggle_shopping_item(item_id: int, checked: Optional[bool] = Query(None)): + if checked is not None: + updated = set_shopping_item_checked(item_id, checked) + else: + updated = toggle_shopping_item(item_id) if not updated: raise HTTPException(status_code=404, detail="Eintrag nicht gefunden.") await shopping_ws_manager.broadcast({"action": "items_updated", "items": get_shopping_list()}) diff --git a/app/templates/shopping_list.html b/app/templates/shopping_list.html index f8b1f92..1bb0b44 100644 --- a/app/templates/shopping_list.html +++ b/app/templates/shopping_list.html @@ -70,6 +70,32 @@ color: #FFF; } + /* Lade-Spinner Animationen */ + @keyframes shopSpin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + .shop-spinner { + display: inline-block; + width: 15px; + height: 15px; + border: 2px solid rgba(255, 255, 255, 0.35); + border-radius: 50%; + border-top-color: #ffffff; + animation: shopSpin 0.7s linear infinite; + vertical-align: middle; + } + .shop-spinner-green { + display: inline-block; + width: 28px; + height: 28px; + border: 3px solid rgba(30, 77, 59, 0.2); + border-radius: 50%; + border-top-color: var(--shop-green); + animation: shopSpin 0.7s linear infinite; + vertical-align: middle; + } + /* Hauptinhalt Container */ .shop-main-container { max-width: 820px; @@ -737,17 +763,41 @@ let ws = null; let reconnectTimeout = null; let pingInterval = null; + let isSyncingQueue = false; - // Server-Daten einbinden + // Server-Daten & Offline-Cache einbinden + let serverItems = []; + let serverCats = []; try { - allItems = {{ items_json | safe }}; - allCategories = {{ categories_json | safe }}; - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); - localStorage.setItem('wtc_shop_cats_cache', JSON.stringify(allCategories)); + serverItems = {{ items_json | safe }}; + serverCats = {{ categories_json | safe }}; } catch (e) { - console.warn("Lade Daten aus Offline-Cache:", e); - allItems = JSON.parse(localStorage.getItem('wtc_shop_items_cache') || '[]'); - allCategories = JSON.parse(localStorage.getItem('wtc_shop_cats_cache') || '[]'); + serverItems = []; + serverCats = []; + } + + const cachedQueue = JSON.parse(localStorage.getItem('wtc_shop_offline_queue') || '[]'); + const cachedItems = JSON.parse(localStorage.getItem('wtc_shop_items_cache') || 'null'); + const cachedCats = JSON.parse(localStorage.getItem('wtc_shop_cats_cache') || 'null'); + + // Wenn offline ODER noch ungesendete Offline-Änderungen vorliegen: Lokalen Cache priorisieren! + if (!navigator.onLine || cachedQueue.length > 0) { + allItems = (cachedItems !== null) ? cachedItems : (serverItems || []); + allCategories = (cachedCats !== null) ? cachedCats : (serverCats || []); + } else { + allItems = (serverItems && serverItems.length) ? serverItems : (cachedItems || []); + allCategories = (serverCats && serverCats.length) ? serverCats : (cachedCats || []); + saveLocalCache(allItems); + localStorage.setItem('wtc_shop_cats_cache', JSON.stringify(allCategories)); + } + + function saveLocalCache(items) { + allItems = items; + try { + localStorage.setItem('wtc_shop_items_cache', JSON.stringify(items)); + } catch (e) { + console.warn('LocalStorage voll oder nicht verfügbar:', e); + } } // ========================================== @@ -801,14 +851,18 @@ try { const data = JSON.parse(event.data); if (data.action === 'items_updated') { + if (isSyncingQueue) return; allItems = data.items; - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); + saveLocalCache(allItems); renderShoppingList(); } else if (data.action === 'categories_updated') { + if (isSyncingQueue) return; allCategories = data.categories; - if (data.items) allItems = data.items; + if (data.items) { + allItems = data.items; + saveLocalCache(allItems); + } localStorage.setItem('wtc_shop_cats_cache', JSON.stringify(allCategories)); - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); renderCategoryFilterChips(); renderShoppingList(); } @@ -873,22 +927,29 @@ } async function flushOfflineQueue() { + if (isSyncingQueue) return; const q = getOfflineQueue(); if (!q.length) return; - localStorage.removeItem('wtc_shop_offline_queue'); - for (const act of q) { - try { - await fetch(act.url, { - method: act.method, - headers: { 'Content-Type': 'application/json' }, - body: act.body ? JSON.stringify(act.body) : undefined - }); - } catch (e) { - console.warn("Fehler beim Nachholen einer Offline-Aktion:", e); + isSyncingQueue = true; + try { + for (const act of q) { + try { + await fetch(act.url, { + method: act.method, + headers: { 'Content-Type': 'application/json' }, + body: act.body ? JSON.stringify(act.body) : undefined + }); + } catch (e) { + console.warn("Fehler beim Nachholen einer Offline-Aktion:", e); + } } + localStorage.removeItem('wtc_shop_offline_queue'); + await refreshDataFromServer(); + showToast('✓ Offline-Änderungen synchronisiert'); + } finally { + isSyncingQueue = false; } - refreshDataFromServer(); } async function refreshDataFromServer() { @@ -1085,14 +1146,14 @@ // Optimistic UI Update item.checked = !item.checked; + saveLocalCache(allItems); renderShoppingList(); try { - const res = await fetch(`/api/shopping-list/${itemId}/toggle`, { method: 'POST' }); + const res = await fetch(`/api/shopping-list/${itemId}/toggle?checked=${item.checked}`, { method: 'POST' }); if (!res.ok) throw new Error('Fehler beim Abhaken'); } catch (err) { - pushOfflineQueue({ method: 'POST', url: `/api/shopping-list/${itemId}/toggle` }); - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); + pushOfflineQueue({ method: 'POST', url: `/api/shopping-list/${itemId}/toggle?checked=${item.checked}` }); } } @@ -1101,6 +1162,7 @@ // Optimistic remove allItems = allItems.filter(it => it.id !== itemId); + saveLocalCache(allItems); renderShoppingList(); showToast('Eintrag gelöscht'); @@ -1109,7 +1171,6 @@ if (!res.ok) throw new Error('Fehler beim Löschen'); } catch (err) { pushOfflineQueue({ method: 'DELETE', url: `/api/shopping-list/${itemId}` }); - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); } } @@ -1121,6 +1182,7 @@ } allItems = allItems.filter(it => !it.checked); + saveLocalCache(allItems); renderShoppingList(); showToast(`${checkedCount} erledigte Artikel gelöscht`); @@ -1128,7 +1190,6 @@ await fetch('/api/shopping-list/checked', { method: 'DELETE' }); } catch (err) { pushOfflineQueue({ method: 'DELETE', url: '/api/shopping-list/checked' }); - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); } } @@ -1176,7 +1237,7 @@ const data = await res.json(); if (data.item) { allItems.push(data.item); - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); + saveLocalCache(allItems); renderShoppingList(); renderCategoryFilterChips(); } @@ -1197,8 +1258,9 @@ }; allItems.push(tempItem); pushOfflineQueue({ method: 'POST', url: '/api/shopping-list', body: payload }); - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); + saveLocalCache(allItems); renderShoppingList(); + renderCategoryFilterChips(); } showToast('Eintrag hinzugefügt'); } @@ -1242,6 +1304,7 @@ item.amount = amount; item.unit = unit; item.note = note; + saveLocalCache(allItems); renderShoppingList(); renderCategoryFilterChips(); } @@ -1262,7 +1325,6 @@ }); } catch (err) { pushOfflineQueue({ method: 'PUT', url: `/api/shopping-list/${id}`, body: payload }); - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); } showToast('Eintrag aktualisiert'); } @@ -1289,6 +1351,7 @@ async function confirmClearAllItems() { closeModal('modal-delete-step2'); allItems = []; + saveLocalCache([]); renderShoppingList(); renderCategoryFilterChips(); showToast('Einkaufsliste komplett geleert'); @@ -1297,7 +1360,6 @@ await fetch('/api/shopping-list/all', { method: 'DELETE' }); } catch (err) { pushOfflineQueue({ method: 'DELETE', url: '/api/shopping-list/all' }); - localStorage.setItem('wtc_shop_items_cache', JSON.stringify([])); } } @@ -1317,8 +1379,9 @@ } body.innerHTML = ` -
- ⏳ Zutaten des Wochenplans werden geladen... +
+
+
Zutaten des Wochenplans werden geladen...
`; @@ -1448,6 +1511,12 @@ const cbs = Array.from(document.querySelectorAll('.mealplan-ing-checkbox:checked')); if (cbs.length === 0) return; + const confirmBtn = document.getElementById('btn-confirm-import-mealplan'); + if (confirmBtn) { + confirmBtn.disabled = true; + confirmBtn.innerHTML = ' Wird übernommen...'; + } + const itemsToAdd = cbs.map(cb => ({ item_name: cb.dataset.name, amount: cb.dataset.amount || '', @@ -1457,8 +1526,6 @@ note: '' })); - closeModal('modal-import-mealplan'); - try { const res = await fetch('/api/shopping-list/bulk', { method: 'POST', @@ -1472,16 +1539,25 @@ } else { await refreshDataFromServer(); } - localStorage.setItem('wtc_shop_items_cache', JSON.stringify(allItems)); + saveLocalCache(allItems); + closeModal('modal-import-mealplan'); renderCategoryFilterChips(); renderShoppingList(); showToast(`✓ ${itemsToAdd.length} Zutaten übernommen`); } else { showToast('Fehler beim Übernehmen'); + if (confirmBtn) { + confirmBtn.disabled = false; + confirmBtn.textContent = 'Ausgewählte übernehmen'; + } } } catch (err) { console.error("Fehler beim Importieren der Zutaten:", err); showToast('Fehler beim Übernehmen'); + if (confirmBtn) { + confirmBtn.disabled = false; + confirmBtn.textContent = 'Ausgewählte übernehmen'; + } } }