diff --git a/index.html b/index.html index 8a4a956..b707d96 100644 --- a/index.html +++ b/index.html @@ -465,10 +465,10 @@ #app-bottom-nav .bottom-dock { position: relative; box-sizing: border-box; - width: min(calc(100% - 3.6rem), 20.9rem); + width: min(calc(100% - 2.4rem), 24.5rem); height: 3.34rem; display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(0, 1fr)); align-items: stretch; gap: 0.06rem; padding: 0.22rem; @@ -547,7 +547,7 @@ padding-inline: 0.7rem; } #app-bottom-nav .bottom-dock { - width: min(calc(100% - 2.4rem), 19.7rem); + width: min(calc(100% - 1.6rem), 22.5rem); height: 3.12rem; gap: 0.05rem; padding: 0.2rem; diff --git a/js/app.js b/js/app.js index 0c70179..ebd694c 100644 --- a/js/app.js +++ b/js/app.js @@ -13,6 +13,9 @@ let setupMealPlanner; let getPantryHTML; let refreshPantry; let setupPantry; +let getShoppingListHTML; +let refreshShoppingList; +let setupShoppingList; let getMealPlanEditorHTML; let setupMealPlanEditor; let getBottomNavHTML; @@ -24,6 +27,7 @@ const moduleLoadPromise = Promise.all([ import(`./views/RecipeDetailV2.js?v=${APP_ASSET_VERSION}`), import(`./views/MealPlanner.js?v=${APP_ASSET_VERSION}`), import(`./views/Pantry.js?v=${APP_ASSET_VERSION}`), + import(`./views/ShoppingList.js?v=${APP_ASSET_VERSION}`), import(`./ui/mealPlanEditor.js?v=${APP_ASSET_VERSION}`), import(`./ui/bottomNav.js?v=${APP_ASSET_VERSION}`), ]).then(([ @@ -32,6 +36,7 @@ const moduleLoadPromise = Promise.all([ recipeDetailModule, mealPlannerModule, pantryModule, + shoppingListModule, mealPlanEditorModule, bottomNavModule, ]) => { @@ -40,6 +45,7 @@ const moduleLoadPromise = Promise.all([ ({ getRecipeDetailHTML, setupRecipeDetail } = recipeDetailModule); ({ getMealPlannerHTML, setupMealPlanner } = mealPlannerModule); ({ getPantryHTML, refreshPantry, setupPantry } = pantryModule); + ({ getShoppingListHTML, refreshShoppingList, setupShoppingList } = shoppingListModule); ({ getMealPlanEditorHTML, setupMealPlanEditor } = mealPlanEditorModule); ({ getBottomNavHTML, setupBottomNav } = bottomNavModule); }); @@ -82,6 +88,7 @@ async function initApp() { ${getRecipeListHTML()} ${getMealPlannerHTML()} ${getPantryHTML()} + ${getShoppingListHTML()} ${getBottomNavHTML()} ${getRecipeDetailHTML()} ${getFilterHTML()} @@ -89,10 +96,11 @@ async function initApp() { ${getAppToastHTML()} `; - setupBottomNav({ refreshPantry }); + setupBottomNav({ refreshPantry, refreshShoppingList }); setupRecipeList(); setupMealPlanner(); setupPantry(); + setupShoppingList(); setupFilter(); setupMealPlanEditor(); setupRecipeDetail(); diff --git a/js/ui/bottomNav.js b/js/ui/bottomNav.js index a709343..016dbea 100644 --- a/js/ui/bottomNav.js +++ b/js/ui/bottomNav.js @@ -42,6 +42,12 @@ export function getBottomNavHTML() { + + + + + +
+
+
+ `; +} + +/* ══════════════════════ RENDERING ══════════════════════ */ + +function getKitchenItems() { + const state = loadShoppingState(); + const list = state.lists.find((l) => l.id === KITCHEN_LIST_ID && l.type === 'kitchen'); + return list ? /** @type {import('../services/pantryShopping.js').KitchenShoppingItem[]} */ (list.items) : []; +} + +function groupItemsByCategory(items) { + /** @type {Map} */ + const groups = new Map(); + for (const item of items) { + const cat = item.category || 'inne'; + if (!groups.has(cat)) groups.set(cat, []); + groups.get(cat).push(item); + } + return CATEGORY_ORDER + .filter((cat) => groups.has(cat)) + .map((cat) => ({ cat, items: groups.get(cat) })); +} + +function itemRowHtml(item) { + const def = INGREDIENTS[item.ingredientId]; + const icon = def ? (CATEGORY_ICONS[def.category] || 'fa-jar') : 'fa-jar'; + const image = def?.image; + const checked = item.checked; + + const mediaHtml = image + ? `` + : `
`; + + return ` +
+ + ${mediaHtml} +
+ ${esc(item.name)} +
+ ${esc(formatQty(item.amount))} ${esc(unitLabel(item.unit))} + +
`; +} + +function renderBoard() { + const root = document.getElementById('sl-board'); + if (!root) return; + + const items = getKitchenItems(); + const unchecked = items.filter((i) => !i.checked); + const checked = items.filter((i) => i.checked); + + if (items.length === 0) { + root.innerHTML = ` +
+
+ +
+

Lista jest pusta

+

Kliknij „Generuj braki z planera" aby dodać składniki na bieżący tydzień.

+
`; + return; + } + + let html = ''; + + // Unchecked items grouped by category + if (unchecked.length > 0) { + const groups = groupItemsByCategory(unchecked); + html += groups.map(({ cat, items: catItems }) => { + const icon = CATEGORY_ICONS[cat] || 'fa-jar'; + return ` +
+
+ +

${esc(categoryLabel(cat))}

+ ${catItems.length} +
+ ${catItems.map((item) => itemRowHtml(item)).join('')} +
`; + }).join(''); + } + + // Checked items at the bottom + if (checked.length > 0) { + html += ` +
+
+ +

Kupione

+ ${checked.length} +
+ ${checked.map((item) => itemRowHtml(item)).join('')} +
`; + } + + root.innerHTML = html; + bindRowEvents(root); +} + +/* ══════════════════════ EVENTS ══════════════════════ */ + +function bindRowEvents(root) { + root.querySelectorAll('.sl-row').forEach((row) => { + const id = row.dataset.id; + + row.querySelector('.sl-check')?.addEventListener('click', () => { + handleToggle(id); + }); + + row.querySelector('.sl-remove')?.addEventListener('click', () => { + removeItemFromList(KITCHEN_LIST_ID, id); + renderBoard(); + updateBadge(); + }); + }); +} + +function handleToggle(itemId) { + const state = loadShoppingState(); + const list = state.lists.find((l) => l.id === KITCHEN_LIST_ID); + if (!list) return; + const item = list.items.find((i) => i.id === itemId); + if (!item) return; + + const wasChecked = item.checked; + item.checked = !wasChecked; + saveShoppingState(state); + + if (!wasChecked) { + // Just checked → apply to pantry + applyItemToPantry(item); + } + + renderBoard(); + updateBadge(); + + if (typeof window.refreshPantry === 'function') window.refreshPantry(); +} + +function applyItemToPantry(item) { + const def = INGREDIENTS[item.ingredientId]; + if (!def) return; + + const pantry = loadPantry(); + + if (item.productId) { + let val = pantry[item.ingredientId]; + if (!val || typeof val === 'number') { + val = { items: [], _total: 0 }; + pantry[item.ingredientId] = val; + } + const idx = val.items.findIndex((i) => i.productId === item.productId); + if (idx >= 0) val.items[idx].qty = Math.round((val.items[idx].qty + item.amount) * 1000) / 1000; + else val.items.push({ productId: item.productId, qty: Math.round(item.amount * 1000) / 1000 }); + val._total = Math.round(val.items.reduce((s, i) => s + i.qty, 0) * 1000) / 1000; + } else { + const cur = typeof pantry[item.ingredientId] === 'number' ? pantry[item.ingredientId] : 0; + pantry[item.ingredientId] = Math.round((cur + item.amount) * 1000) / 1000; + } + + savePantry(pantry); +} + +function handleGenerate() { + const plans = loadPlans(); + const weekStart = startOfWeekMonday(new Date()); + const needLines = aggregateWeekIngredientNeed(plans, weekStart); + const pantry = loadPantry(); + const shortfalls = computeShortfalls(needLines, pantry); + + if (shortfalls.length === 0) { + showAppToast('Wszystko masz w spiżarni!'); + return; + } + + const lines = shortfalls.map((s) => ({ + ingredientId: s.ingredientId, + amount: s.shortfall, + unit: s.unit, + name: s.name, + category: s.category, + sourceNote: 'Z planera', + })); + + addOrMergeShoppingLines(lines, KITCHEN_LIST_ID); + renderBoard(); + updateBadge(); + showAppToast(`Dodano ${shortfalls.length} pozycji z planera`); +} + +function handleClearChecked() { + clearCheckedInList(KITCHEN_LIST_ID); + renderBoard(); + updateBadge(); +} + +/* ══════════════════════ BADGE ══════════════════════ */ + +function updateBadge() { + const items = getKitchenItems(); + const uncheckedCount = items.filter((i) => !i.checked).length; + const badge = document.getElementById('nav-shopping-badge'); + if (!badge) return; + if (uncheckedCount > 0) { + badge.textContent = String(uncheckedCount > 99 ? '99+' : uncheckedCount); + badge.classList.remove('hidden'); + } else { + badge.classList.add('hidden'); + } +} + +/* ══════════════════════ PUBLIC API ══════════════════════ */ + +export function refreshShoppingList() { + renderBoard(); + updateBadge(); +} + +export function setupShoppingList() { + renderBoard(); + updateBadge(); + + document.getElementById('sl-generate')?.addEventListener('click', handleGenerate); + document.getElementById('sl-clear-checked')?.addEventListener('click', handleClearChecked); + + window.refreshShoppingList = refreshShoppingList; +}