Fix offline sync priority, idempotent check state, and add loading spinner for meal plan import
This commit is contained in:
@@ -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 = ?"
|
||||
|
||||
+6
-2
@@ -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()})
|
||||
|
||||
@@ -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 = `
|
||||
<div style="text-align: center; padding: 25px; color: #666;">
|
||||
<span>⏳ Zutaten des Wochenplans werden geladen...</span>
|
||||
<div style="text-align: center; padding: 35px 20px; color: #555;">
|
||||
<div class="shop-spinner shop-spinner-green" style="margin-bottom: 12px;"></div>
|
||||
<div style="font-weight: 600; font-size: 0.95rem; color: var(--shop-green);">Zutaten des Wochenplans werden geladen...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -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 = '<span class="shop-spinner" style="margin-right: 6px;"></span> <span>Wird übernommen...</span>';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user