diff --git a/js/views/Pantry.js b/js/views/Pantry.js index 7dbcc22..b289b14 100644 --- a/js/views/Pantry.js +++ b/js/views/Pantry.js @@ -6,7 +6,15 @@ import { getProductsForIngredient, ingredientHasProducts, } from '../data/catalog.js?v=8'; -import { addIngredientToKitchenList, addOrMergeShoppingLines, categoryLabel, loadPantry, setPantryQty, setPantryProductQty, getPantryTotal, getPantryProducts } from '../services/pantryShopping.js?v=2'; +import { + addOrMergeShoppingLines, + categoryLabel, + loadPantry, + setPantryQty, + setPantryProductQty, + getPantryTotal, + getPantryProducts, +} from '../services/pantryShopping.js?v=2'; import { showAppToast } from '../ui/toast.js'; /* ── helpers ── */ @@ -23,6 +31,66 @@ function normalizeSearch(q) { return String(q).trim().toLowerCase(); } +function formatQty(n) { + const rounded = Math.round((Number(n) || 0) * 10) / 10; + return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1).replace(/\.0$/, ''); +} + +function formatQtyWithUnit(qty, unit) { + return `${formatQty(qty)} ${unitLabel(unit)}`; +} + +function productCountLabel(count) { + if (count === 1) return '1 produkt'; + const mod10 = count % 10; + const mod100 = count % 100; + if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) return `${count} produkty`; + return `${count} produktów`; +} + +function productCountShortLabel(count) { + return `${count} prod.`; +} + +function nutritionForQty(def, qty, nutrition = def?.nutritionPer100g) { + if (!def || !nutrition || !Number.isFinite(qty) || qty <= 0) return null; + let grams = qty; + if (def.pantryUnit === 'szt' && def.weightPerPiece) grams = qty * def.weightPerPiece; + const f = grams / 100; + return { + kcal: Math.round(nutrition.kcal * f), + protein: Math.round(nutrition.protein * f * 10) / 10, + fat: Math.round(nutrition.fat * f * 10) / 10, + carbs: Math.round(nutrition.carbs * f * 10) / 10, + }; +} + +function macroLine(n) { + if (!n) return ''; + return `${n.kcal} kcal · ${formatQty(n.protein)}g B · ${formatQty(n.fat)}g T · ${formatQty(n.carbs)}g W`; +} + +function mediaHtml(image, icon, sizeClass = 'w-11 h-11', radiusClass = 'rounded-2xl') { + if (image) { + return ``; + } + return `
`; +} + +function compactMetaText(text, tone = 'default') { + const color = tone === 'success' ? '#6ee7b7' : tone === 'muted' ? '#9b978f' : '#d7d2c8'; + return `${esc(text)}`; +} + +function sortProductsByStock(products, pantryItems) { + return [...products].sort((a, b) => { + const aq = pantryItems.find((i) => i.productId === a.id)?.qty || 0; + const bq = pantryItems.find((i) => i.productId === b.id)?.qty || 0; + if (bq !== aq) return bq - aq; + return a.name.localeCompare(b.name, 'pl'); + }); +} + const CATEGORY_ICONS = { pieczywo: 'fa-bread-slice', nabial: 'fa-cheese', @@ -38,16 +106,9 @@ const SEARCH_SHELL_SHADOW = '0 5px 10px rgba(0,0,0,0.16), 0 14px 22px rgba(0,0,0 /* ── state ── */ -let showOnlyStock = false; let editingId = null; -/** @type {Set} */ -const selectedCategories = new Set(); - -let editShopStep = 1; -let editShopUsesPacks = false; - -const BOTTOM = '5.25rem'; -const HIDDEN_Y = `translateY(calc(100% + ${BOTTOM}))`; +let editingProductId = null; +let cardCloseTimer = null; /* ══════════════════════ HTML SHELL ══════════════════════ */ @@ -59,32 +120,7 @@ export function getPantryHTML() {
- -
-
- - - @@ -94,14 +130,17 @@ export function getPantryHTML() { - + `; } function groupByCategory(ids) { @@ -298,7 +229,7 @@ function groupByCategory(ids) { } return [...groups.keys()] .sort((a, b) => categoryLabel(a).localeCompare(categoryLabel(b))) - .map(cat => ({ cat, ids: groups.get(cat) })); + .map((cat) => ({ cat, ids: groups.get(cat) })); } function renderBoard() { @@ -307,133 +238,133 @@ function renderBoard() { const q = document.getElementById('pantry-search')?.value || ''; const pantry = loadPantry(); - const allFiltered = getFilteredIds(q); - const visible = showOnlyStock - ? allFiltered.filter(id => getPantryTotal(id, pantry) > 0) - : allFiltered; + const visible = getFilteredIds(q); if (visible.length === 0) { - root.innerHTML = showOnlyStock - ? `
-
- -
-

Nic na stanie

-

Wyłącz filtr, aby zobaczyć cały katalog produktów

-
` - : `

Brak wyników — zmień wyszukiwanie lub filtry.

`; + root.innerHTML = `

Brak wyników — zmień wyszukiwanie.

`; return; } const groups = groupByCategory(visible); - let html = ''; - for (const { cat, ids } of groups) { + root.innerHTML = groups.map(({ cat, ids }) => { const icon = CATEGORY_ICONS[cat] || 'fa-jar'; - html += ` -
-

- ${esc(categoryLabel(cat))} -

-
${ids.map(id => ingredientRowHtml(id, pantry)).join('')}
-
`; - } + return ` +
+
+
+

+ ${esc(categoryLabel(cat))} +

+

${ids.length} ${getCategoryItemLabel(ids.length)}

+
+ przesuń w bok +
+
+
+ ${ids.map((rowId) => ingredientRowHtml(rowId, pantry)).join('')} +
+
+
`; + }).join(''); - root.innerHTML = html; - - root.querySelectorAll('.pv2-chip').forEach(btn => { + root.querySelectorAll('.pv2-chip').forEach((btn) => { btn.addEventListener('click', () => openIngredientCard(btn.dataset.id, null)); }); - root.querySelectorAll('.pv2-product-row').forEach(btn => { - btn.addEventListener('click', () => openIngredientCard(btn.dataset.id, btn.dataset.productId)); - }); } -/* ══════════════════════ STOCK TOGGLE ══════════════════════ */ +/* ══════════════════════ INGREDIENT SHEET ══════════════════════ */ - -/* ══════════════════════ INGREDIENT CARD ══════════════════════ */ - -let editingProductId = null; - -function openIngredientCard(ingredientId, productId) { - const def = INGREDIENTS[ingredientId]; - if (!def) return; - editingId = ingredientId; - editingProductId = productId || null; - - const product = editingProductId ? PRODUCTS[editingProductId] : null; - const pantry = loadPantry(); - const u = unitLabel(def.pantryUnit); +function renderCardHeader(def, product, pantry) { + const hasProducts = ingredientHasProducts(def.id); const icon = CATEGORY_ICONS[def.category] || 'fa-jar'; - - // Hero image — product image > ingredient image > fallback const image = product?.image || def.image; const img = document.getElementById('pv2-card-img'); const fallback = document.getElementById('pv2-card-fallback'); const fallbackIcon = document.getElementById('pv2-card-fallback-icon'); - if (image) { - img.src = image; img.alt = product?.name || def.name; img.classList.remove('hidden'); fallback.classList.add('hidden'); - } else { - img.classList.add('hidden'); fallback.classList.remove('hidden'); - if (fallbackIcon) fallbackIcon.className = `fas ${icon} text-3xl`; + if (img && fallback) { + if (image) { + img.src = image; + img.alt = product?.name || def.name; + img.classList.remove('hidden'); + fallback.classList.add('hidden'); + } else { + img.classList.add('hidden'); + fallback.classList.remove('hidden'); + if (fallbackIcon) fallbackIcon.className = `fas ${icon} text-3xl`; + } } - // Header — show product info or ingredient info - document.getElementById('pv2-card-category').textContent = product?.brand || categoryLabel(def.category); - document.getElementById('pv2-card-name').textContent = product?.name || def.name; - const packEl = document.getElementById('pv2-card-pack'); - const packLabel = product?.packLabel || def.purchasePack?.label; - if (packLabel) { - packEl.textContent = packLabel; - packEl.classList.remove('hidden'); - } else { - packEl.classList.add('hidden'); + const totalQty = getPantryTotal(def.id, pantry); + const productQty = product + ? (getPantryProducts(def.id, pantry).find((i) => i.productId === product.id)?.qty || 0) + : totalQty; + const stockNut = nutritionForQty(def, productQty, product?.nutritionPer100g || def.nutritionPer100g); + + const categoryEl = document.getElementById('pv2-card-category'); + const nameEl = document.getElementById('pv2-card-name'); + const subtitleEl = document.getElementById('pv2-card-subtitle'); + const backBtn = document.getElementById('pv2-card-back'); + + if (categoryEl) categoryEl.textContent = product?.brand || categoryLabel(def.category); + if (nameEl) nameEl.textContent = product?.name || def.name; + + if (subtitleEl) { + let subtitle = ''; + if (product) { + subtitle = [def.name, product.packLabel].filter(Boolean).join(' • '); + } else if (hasProducts) { + subtitle = `${productCountLabel(getProductsForIngredient(def.id).length)} • ${formatQtyWithUnit(totalQty, def.pantryUnit)} na stanie`; + } else if (def.purchasePack?.label) { + subtitle = def.purchasePack.label; + } + + if (subtitle) { + subtitleEl.textContent = subtitle; + subtitleEl.classList.remove('hidden'); + } else { + subtitleEl.classList.add('hidden'); + } } - // Nutrition — use product values if available - renderCardNutrition(def, product); - - // Stock - renderCardStock(ingredientId, editingProductId, pantry); - - // Shopping - renderCardShop(ingredientId, editingProductId); - - // Show - const overlay = document.getElementById('pv2-card-overlay'); - if (overlay) { overlay.classList.remove('hidden'); overlay.style.pointerEvents = 'auto'; } -} - -function closeIngredientCard() { - const overlay = document.getElementById('pv2-card-overlay'); - if (overlay) { overlay.classList.add('hidden'); overlay.style.pointerEvents = 'none'; } - editingId = null; - renderBoard(); + if (backBtn) { + backBtn.classList.toggle('hidden', !(hasProducts && product)); + } } function renderCardNutrition(def, product) { const wrap = document.getElementById('pv2-card-nutrition'); if (!wrap) return; const n = product?.nutritionPer100g || def.nutritionPer100g; - if (!n) { wrap.innerHTML = ''; return; } - const label = def.pantryUnit === 'ml' ? 'na 100 ml' : 'na 100 g'; + if (!n) { + wrap.innerHTML = ''; + return; + } + const hasProducts = ingredientHasProducts(def.id); + const unitScope = def.pantryUnit === 'ml' ? 'na 100 ml' : 'na 100 g'; + const hint = product + ? 'dokładne dla produktu' + : hasProducts + ? 'orientacyjnie dla składnika' + : 'bazowe wartości'; + wrap.innerHTML = ` -

${esc(label)}

+

Wartości odżywcze

+

${esc(unitScope)} • ${esc(hint)}

${n.kcal}

kcal

-

${n.protein}g

+

${formatQty(n.protein)}g

białko

-

${n.fat}g

+

${formatQty(n.fat)}g

tłuszcz

-

${n.carbs}g

+

${formatQty(n.carbs)}g

węgl.

`; @@ -444,50 +375,120 @@ function renderCardStock(ingredientId, productId, pantry) { if (!wrap) return; const def = INGREDIENTS[ingredientId]; if (!def) return; + + const hasProducts = ingredientHasProducts(ingredientId); + const product = productId ? PRODUCTS[productId] : null; + const totalQty = getPantryTotal(ingredientId, pantry); const u = unitLabel(def.pantryUnit); - let html = `

Zapas

`; - - if (productId) { - // Product card — show just this product's stock - const product = PRODUCTS[productId]; - const pantryItems = getPantryProducts(ingredientId, pantry); - const qty = pantryItems.find(i => i.productId === productId)?.qty || 0; - const step = product?.packSize || pantryQtyStep(ingredientId); - html += `
- - ${Math.round(qty)} ${esc(u)} - -
`; - } else { - // Generic ingredient — simple +/- - const qty = getPantryTotal(ingredientId, pantry); - const step = pantryQtyStep(ingredientId); - html += `
- - ${Math.round(qty)} ${esc(u)} - -
`; + if (hasProducts && !product) { + const stockedCount = getPantryProducts(ingredientId, pantry).filter((i) => i.qty > 0).length; + const summaryNutrition = nutritionForQty(def, totalQty); + wrap.innerHTML = ` +

Zapas

+
+
+
+

${esc(formatQty(totalQty))} ${esc(u)}

+

${stockedCount} z ${getProductsForIngredient(ingredientId).length} produktów ma stan

+
+ Wybierz produkt niżej, aby zmienić stan +
+ ${summaryNutrition ? `

${esc(macroLine(summaryNutrition))}

` : ''} +
`; + return; } - wrap.innerHTML = html; + const qty = product + ? (getPantryProducts(ingredientId, pantry).find((i) => i.productId === productId)?.qty || 0) + : totalQty; + const step = product ? (product.packSize || pantryQtyStep(ingredientId)) : pantryQtyStep(ingredientId); + const stockNut = nutritionForQty(def, qty, product?.nutritionPer100g || def.nutritionPer100g); - wrap.querySelectorAll('.pv2-stock-btn').forEach(btn => { + wrap.innerHTML = ` +

Zapas

+
+ +
+

${esc(formatQty(qty))} ${esc(u)}

+

Krok: ${esc(formatQty(step))} ${esc(u)}

+
+ +
+ ${stockNut ? `

${esc(macroLine(stockNut))} na stanie

` : ''}`; + + wrap.querySelectorAll('.pv2-stock-btn').forEach((btn) => { btn.addEventListener('click', () => { if (!editingId) return; const pid = btn.dataset.pid; - const step = Number(btn.dataset.step) || 1; - const dir = Number(btn.dataset.dir); - const p = loadPantry(); + const stepVal = Number(btn.dataset.step) || 1; + const dir = Number(btn.dataset.dir) || 1; + const currentPantry = loadPantry(); if (pid === '_generic') { - const cur = getPantryTotal(editingId, p); - setPantryQty(editingId, Math.max(0, cur + step * dir)); + const cur = getPantryTotal(editingId, currentPantry); + setPantryQty(editingId, Math.max(0, cur + stepVal * dir)); } else { - const items = getPantryProducts(editingId, p); - const cur = items.find(i => i.productId === pid)?.qty || 0; - setPantryProductQty(editingId, pid, Math.max(0, cur + step * dir)); + const items = getPantryProducts(editingId, currentPantry); + const cur = items.find((i) => i.productId === pid)?.qty || 0; + setPantryProductQty(editingId, pid, Math.max(0, cur + stepVal * dir)); } - renderCardStock(editingId, editingProductId, loadPantry()); + renderBoard(); + renderIngredientCard(); + }); + }); +} + +function productSwitcherRowHtml(ingredientId, productId, pantry, selectedProductId) { + const def = INGREDIENTS[ingredientId]; + const product = PRODUCTS[productId]; + const icon = CATEGORY_ICONS[def.category] || 'fa-jar'; + const qty = getPantryProducts(ingredientId, pantry).find((i) => i.productId === productId)?.qty || 0; + const isSelected = selectedProductId === productId; + return ``; +} + +function renderCardProducts(ingredientId, productId, pantry) { + const wrap = document.getElementById('pv2-card-products-section'); + if (!wrap) return; + if (!ingredientHasProducts(ingredientId)) { + wrap.innerHTML = ''; + return; + } + + const products = sortProductsByStock(getProductsForIngredient(ingredientId), getPantryProducts(ingredientId, pantry)); + const title = 'Produkty'; + const subtitle = productId + ? 'Wróć lub wybierz inny wariant.' + : 'Wybierz wariant, aby zobaczyć szczegóły.'; + + wrap.innerHTML = ` +

${esc(title)}

+

${esc(subtitle)}

+
+ ${products.map((p) => productSwitcherRowHtml(ingredientId, p.id, pantry, productId)).join('')} +
`; + + wrap.querySelectorAll('.pv2-card-product-row').forEach((btn) => { + btn.addEventListener('click', () => { + editingProductId = btn.dataset.productId || null; + renderIngredientCard(); }); }); } @@ -497,14 +498,24 @@ function renderCardShop(ingredientId, productId) { if (!wrap) return; const def = INGREDIENTS[ingredientId]; if (!def) return; - const u = unitLabel(def.pantryUnit); + + const hasProducts = ingredientHasProducts(ingredientId); const product = productId ? PRODUCTS[productId] : null; const packSize = product?.packSize || def.purchasePack?.amount; const packLabel = product?.packLabel || def.purchasePack?.label; const usesPacks = Boolean(packSize && packSize > 0); - const btnLabel = usesPacks ? `Dodaj na listę (${packLabel || `${packSize} ${u}`})` : 'Dodaj na listę'; + const btnLabel = usesPacks + ? `Dodaj na listę (${packLabel || `${formatQty(packSize)} ${unitLabel(def.pantryUnit)}`})` + : 'Dodaj na listę'; + const helperText = hasProducts && !product + ? 'Doda składnik bez wskazanej marki. Jeśli chcesz konkretny produkt, wybierz go wyżej.' + : product + ? 'Pozycja trafi na listę zakupów z dokładnym produktem.' + : 'Szybki skrót do listy zakupów ze spiżarni.'; wrap.innerHTML = ` +

Lista zakupów

+

${esc(helperText)}

`; @@ -515,9 +526,7 @@ function renderCardShop(ingredientId, productId) { if (!d) return; const uLabel = unitLabel(d.pantryUnit); const amt = usesPacks ? packSize : pantryQtyStep(editingId); - const note = usesPacks ? (packLabel || `${packSize} ${uLabel}`) : undefined; - - // Use addOrMergeShoppingLines to include productId + const note = usesPacks ? (packLabel || `${formatQty(packSize)} ${uLabel}`) : undefined; const line = { ingredientId: editingId, amount: amt, @@ -528,14 +537,66 @@ function renderCardShop(ingredientId, productId) { }; if (productId) line.productId = productId; addOrMergeShoppingLines([line]); - showAppToast(`Dodano ${product?.name || d.name} na listę.`); window.refreshShopping?.(); }); } +function renderIngredientCard() { + if (!editingId) return; + const def = INGREDIENTS[editingId]; + if (!def) return; + const product = editingProductId ? PRODUCTS[editingProductId] : null; + const pantry = loadPantry(); + + renderCardHeader(def, product, pantry); + renderCardNutrition(def, product); + renderCardStock(editingId, editingProductId, pantry); + renderCardProducts(editingId, editingProductId, pantry); + renderCardShop(editingId, editingProductId); +} + +function openIngredientCard(ingredientId, productId) { + const def = INGREDIENTS[ingredientId]; + if (!def) return; + editingId = ingredientId; + editingProductId = productId && PRODUCTS[productId] ? productId : null; + renderIngredientCard(); + + const overlay = document.getElementById('pv2-card-overlay'); + const card = document.getElementById('pv2-card'); + if (!overlay || !card) return; + clearTimeout(cardCloseTimer); + overlay.classList.remove('hidden'); + overlay.style.pointerEvents = 'auto'; + requestAnimationFrame(() => { + overlay.classList.add('opacity-100'); + card.style.opacity = '1'; + card.style.transform = 'translateY(0)'; + }); +} + +function closeIngredientCard() { + const overlay = document.getElementById('pv2-card-overlay'); + const card = document.getElementById('pv2-card'); + if (overlay && card) { + overlay.classList.remove('opacity-100'); + overlay.style.pointerEvents = 'none'; + card.style.opacity = '0'; + card.style.transform = 'translateY(1.5rem)'; + cardCloseTimer = setTimeout(() => overlay.classList.add('hidden'), 220); + } + editingId = null; + editingProductId = null; + renderBoard(); +} + function bindEditSheet() { document.getElementById('pv2-card-close')?.addEventListener('click', closeIngredientCard); + document.getElementById('pv2-card-back')?.addEventListener('click', () => { + editingProductId = null; + renderIngredientCard(); + }); document.getElementById('pv2-card-overlay')?.addEventListener('click', (e) => { if (e.target.id === 'pv2-card-overlay') closeIngredientCard(); }); @@ -544,52 +605,15 @@ function bindEditSheet() { /* ══════════════════════ PUBLIC API ══════════════════════ */ export function refreshPantry() { - renderCategoryChips(); renderBoard(); + if (editingId) renderIngredientCard(); } export function setupPantry() { - updateFilterBadge(); renderBoard(); bindEditSheet(); document.getElementById('pantry-search')?.addEventListener('input', () => renderBoard()); - // Filter popup - document.getElementById('pantry-filter-btn')?.addEventListener('click', () => { - if (isFilterOpen()) closeFilterPopup(); else openFilterPopup(); - }); - - document.getElementById('pantry-filter-overlay')?.addEventListener('click', (e) => { - if (e.target.id === 'pantry-filter-overlay') closeFilterPopup(); - }); - - document.getElementById('pantry-filter-clear')?.addEventListener('click', () => { - selectedCategories.clear(); - showOnlyStock = false; - renderFilterCategories(); - renderFilterStockCheck(); - updateFilterBadge(); - renderBoard(); - }); - - document.getElementById('pantry-filter-categories')?.addEventListener('click', (e) => { - const btn = e.target.closest('.pv2-filter-cat'); - if (!btn) return; - const cat = btn.dataset.cat; - if (selectedCategories.has(cat)) selectedCategories.delete(cat); - else selectedCategories.add(cat); - renderFilterCategories(); - updateFilterBadge(); - renderBoard(); - }); - - document.getElementById('pantry-filter-stock')?.addEventListener('click', () => { - showOnlyStock = !showOnlyStock; - renderFilterStockCheck(); - updateFilterBadge(); - renderBoard(); - }); - window.refreshPantry = refreshPantry; }