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
`;
}).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)}
${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;
}