import { INGREDIENTS, CATEGORY_LABELS, PRODUCTS, pantryQtyStep, getProductsForIngredient, ingredientHasProducts, } from '../data/catalog.js?v=8'; import { addOrMergeShoppingLines, categoryLabel, loadPantry, setPantryQty, setPantryProductQty, getPantryTotal, getPantryProducts, } from '../services/pantryShopping.js?v=2'; import { showAppToast } from '../ui/toast.js'; /* ── helpers ── */ function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function unitLabel(u) { return u === 'szt' ? 'szt.' : u; } 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', mieso_ryby: 'fa-drumstick-bite', warzywa: 'fa-carrot', owoce: 'fa-apple-whole', suche: 'fa-wheat-awn', przyprawy: 'fa-leaf', inne: 'fa-jar', }; const SEARCH_SHELL_SHADOW = '0 5px 10px rgba(0,0,0,0.16), 0 14px 22px rgba(0,0,0,0.24), 0 22px 34px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.04)'; /* ── state ── */ let editingId = null; let editingProductId = null; let cardCloseTimer = null; /* ══════════════════════ HTML SHELL ══════════════════════ */ export function getPantryHTML() { return ` `; } /* ══════════════════════ BOARD RENDERING ══════════════════════ */ function getFilteredIds(searchRaw) { const q = normalizeSearch(searchRaw); return Object.keys(INGREDIENTS).filter((id) => { const d = INGREDIENTS[id]; if (!q) return true; return d.name.toLowerCase().includes(q) || (CATEGORY_LABELS[d.category] || '').toLowerCase().includes(q); }).sort((a, b) => INGREDIENTS[a].name.localeCompare(INGREDIENTS[b].name, 'pl')); } function getCategoryItemLabel(count) { if (count === 1) return 'składnik'; const mod10 = count % 10; const mod100 = count % 100; if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) return 'składniki'; return 'składników'; } function ingredientRowHtml(id, pantry) { const def = INGREDIENTS[id]; const icon = CATEGORY_ICONS[def.category] || 'fa-jar'; const qty = getPantryTotal(id, pantry); const hasProducts = ingredientHasProducts(id); const products = hasProducts ? getProductsForIngredient(id) : []; const pantryItems = hasProducts ? getPantryProducts(id, pantry) : []; const stockedCount = pantryItems.filter((i) => i.qty > 0).length; const hasStock = qty > 0; const accent = hasStock ? '#6ee7b7' : '#4b4a46'; const qtyColor = hasStock ? '#6ee7b7' : '#d7d2c8'; const subtitle = hasProducts ? `${stockedCount}/${products.length} produktów` : categoryLabel(def.category); const status = hasProducts ? (stockedCount === 0 ? 'wybierz produkt' : stockedCount === products.length ? 'wszystko na stanie' : 'częściowo na stanie') : (hasStock ? 'na stanie' : 'brak'); const badgeText = hasProducts ? productCountShortLabel(products.length) : unitLabel(def.pantryUnit); return ``; } function groupByCategory(ids) { /** @type {Map} */ const groups = new Map(); for (const id of ids) { const cat = INGREDIENTS[id].category; if (!groups.has(cat)) groups.set(cat, []); groups.get(cat).push(id); } return [...groups.keys()] .sort((a, b) => categoryLabel(a).localeCompare(categoryLabel(b))) .map((cat) => ({ cat, ids: groups.get(cat) })); } function renderBoard() { const root = document.getElementById('pantry-board'); if (!root) return; const q = document.getElementById('pantry-search')?.value || ''; const pantry = loadPantry(); const visible = getFilteredIds(q); if (visible.length === 0) { root.innerHTML = `

Brak wyników — zmień wyszukiwanie.

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

${esc(categoryLabel(cat))}

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

przesuń w bok
${ids.map((rowId) => ingredientRowHtml(rowId, pantry)).join('')}
`; }).join(''); root.querySelectorAll('.pv2-chip').forEach((btn) => { btn.addEventListener('click', () => openIngredientCard(btn.dataset.id, null)); }); } /* ══════════════════════ INGREDIENT SHEET ══════════════════════ */ function renderCardHeader(def, product, pantry) { const hasProducts = ingredientHasProducts(def.id); const icon = CATEGORY_ICONS[def.category] || 'fa-jar'; 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 (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`; } } 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'); } } 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 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 = `

Wartości odżywcze

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

${n.kcal}

kcal

${formatQty(n.protein)}g

białko

${formatQty(n.fat)}g

tłuszcz

${formatQty(n.carbs)}g

węgl.

`; } function renderCardStock(ingredientId, productId, pantry) { const wrap = document.getElementById('pv2-card-stock-section'); 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); 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; } 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.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 stepVal = Number(btn.dataset.step) || 1; const dir = Number(btn.dataset.dir) || 1; const currentPantry = loadPantry(); if (pid === '_generic') { const cur = getPantryTotal(editingId, currentPantry); setPantryQty(editingId, Math.max(0, cur + stepVal * dir)); } else { const items = getPantryProducts(editingId, currentPantry); const cur = items.find((i) => i.productId === pid)?.qty || 0; setPantryProductQty(editingId, pid, Math.max(0, cur + stepVal * dir)); } 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(); }); }); } function renderCardShop(ingredientId, productId) { const wrap = document.getElementById('pv2-card-shop-section'); if (!wrap) return; const def = INGREDIENTS[ingredientId]; if (!def) return; 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 || `${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)}

`; document.getElementById('pv2-card-add-list')?.addEventListener('click', () => { if (!editingId) return; const d = INGREDIENTS[editingId]; if (!d) return; const uLabel = unitLabel(d.pantryUnit); const amt = usesPacks ? packSize : pantryQtyStep(editingId); const note = usesPacks ? (packLabel || `${formatQty(packSize)} ${uLabel}`) : undefined; const line = { ingredientId: editingId, amount: amt, unit: uLabel, name: product?.name || d.name, category: d.category, sourceNote: note || 'Ze spiżarni', }; 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(); }); } /* ══════════════════════ PUBLIC API ══════════════════════ */ export function refreshPantry() { renderBoard(); if (editingId) renderIngredientCard(); } export function setupPantry() { renderBoard(); bindEditSheet(); document.getElementById('pantry-search')?.addEventListener('input', () => renderBoard()); window.refreshPantry = refreshPantry; }