Redesign products
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m15s

This commit is contained in:
2026-04-08 16:02:12 +02:00
parent 7db4deee82
commit e2b15956a0
3 changed files with 259 additions and 136 deletions

View File

@@ -383,19 +383,19 @@ export function categoryLabel(cat) {
* Pantry v2 — hybrydowy format.
*
* Wartość dla składnika może być:
* number — składnik generyczny (bez zdefiniowanych produktów)
* { _total, items: [{productId, qty}], generic } — składnik z produktami
* number — składnik bez produktów (generyczny)
* { _total, items: [{productId, qty}] } — składnik z produktami
*
* _total = generic + sum(items[].qty) (cache, zawsze przeliczany przy zapisie)
* Brak pojęcia "generic" — jeśli składnik ma produkty, każda ilość
* musi być przypisana do konkretnego produktu.
* ══════════════════════════════════════════════════════════════════════ */
/** @typedef {{ productId: string, qty: number }} PantryProductItem */
/** @typedef {{ _total: number, items: PantryProductItem[], generic: number }} PantryProductEntry */
/** @typedef {{ _total: number, items: PantryProductItem[] }} PantryProductEntry */
/** @typedef {Record<string, number | PantryProductEntry>} PantryV2 */
function recalcTotal(entry) {
const itemSum = entry.items.reduce((s, i) => s + i.qty, 0);
entry._total = Math.round((entry.generic + itemSum) * 1000) / 1000;
entry._total = Math.round(entry.items.reduce((s, i) => s + i.qty, 0) * 1000) / 1000;
return entry;
}
@@ -406,13 +406,30 @@ function normalizePantryEntry(ingredientId, val) {
.filter(i => i && typeof i.productId === 'string' && PRODUCTS[i.productId] && Number.isFinite(Number(i.qty)) && Number(i.qty) > 0)
.map(i => ({ productId: i.productId, qty: Math.round(Number(i.qty) * 1000) / 1000 }))
: [];
// Migrate generic stock → first product
const generic = Math.max(0, Number(val.generic) || 0);
return recalcTotal({ items, generic, _total: 0 });
if (generic > 0 && ingredientHasProducts(ingredientId)) {
const products = getProductsForIngredient(ingredientId);
if (products.length > 0) {
const firstPid = products[0].id;
const existing = items.find(i => i.productId === firstPid);
if (existing) existing.qty = Math.round((existing.qty + generic) * 1000) / 1000;
else items.push({ productId: firstPid, qty: Math.round(generic * 1000) / 1000 });
}
}
return recalcTotal({ items, _total: 0 });
}
const n = Number(val);
if (!Number.isFinite(n) || n < 0) return null;
if (ingredientHasProducts(ingredientId)) {
return recalcTotal({ items: [], generic: n, _total: 0 });
// Migrate number → first product
if (n > 0) {
const products = getProductsForIngredient(ingredientId);
if (products.length > 0) {
return recalcTotal({ items: [{ productId: products[0].id, qty: n }], _total: 0 });
}
}
return recalcTotal({ items: [], _total: 0 });
}
return n;
}
@@ -480,30 +497,18 @@ export function getPantryProducts(ingredientId, pantry) {
return val.items || [];
}
/** Ilość generyczna (nieprzypisana do produktu). */
/** @deprecated No generic stock for ingredients with products. Returns 0 for those. */
export function getPantryGeneric(ingredientId, pantry) {
const val = pantry[ingredientId];
if (typeof val === 'number') return val;
if (!val) return 0;
return val.generic || 0;
return 0;
}
/** Ustaw ilość generyczną składnika (bez produktu). */
/** Ustaw ilość składnika BEZ produktów. Dla składników z produktami użyj setPantryProductQty. */
export function setPantryQty(ingredientId, qty) {
const pantry = loadPantry();
const val = pantry[ingredientId];
if (val && typeof val === 'object') {
val.generic = Math.max(0, Math.round(qty * 1000) / 1000);
recalcTotal(val);
if (val._total <= 0) delete pantry[ingredientId];
} else {
if (qty <= 0 || !Number.isFinite(qty)) delete pantry[ingredientId];
else if (ingredientHasProducts(ingredientId)) {
pantry[ingredientId] = recalcTotal({ items: [], generic: Math.round(qty * 1000) / 1000, _total: 0 });
} else {
pantry[ingredientId] = Math.round(qty * 1000) / 1000;
}
}
else pantry[ingredientId] = Math.round(qty * 1000) / 1000;
savePantry(pantry);
return pantry;
}
@@ -513,8 +518,7 @@ export function setPantryProductQty(ingredientId, productId, qty) {
const pantry = loadPantry();
let val = pantry[ingredientId];
if (!val || typeof val === 'number') {
const generic = typeof val === 'number' ? val : 0;
val = { items: [], generic, _total: 0 };
val = { items: [], _total: 0 };
pantry[ingredientId] = val;
}
const idx = val.items.findIndex(i => i.productId === productId);
@@ -545,27 +549,21 @@ export function applyCheckedKitchenListToPantry() {
const def = INGREDIENTS[it.ingredientId];
if (!def) continue;
if (normalizeUnitForPantry(it.unit, def.pantryUnit) === null) continue;
const val = pantry[it.ingredientId];
if (val && typeof val === 'object') {
if (it.productId && PRODUCTS[it.productId]) {
// Add to specific product
let val = pantry[it.ingredientId];
if (!val || typeof val === 'number') {
val = { items: [], _total: 0 };
pantry[it.ingredientId] = val;
}
const idx = val.items.findIndex(i => i.productId === it.productId);
if (idx >= 0) val.items[idx].qty = Math.round((val.items[idx].qty + it.amount) * 1000) / 1000;
else val.items.push({ productId: it.productId, qty: Math.round(it.amount * 1000) / 1000 });
} else {
val.generic = Math.round(((val.generic || 0) + it.amount) * 1000) / 1000;
}
recalcTotal(val);
} else if (ingredientHasProducts(it.ingredientId)) {
const cur = typeof val === 'number' ? val : 0;
const entry = { items: [], generic: cur, _total: 0 };
if (it.productId && PRODUCTS[it.productId]) {
entry.items.push({ productId: it.productId, qty: Math.round(it.amount * 1000) / 1000 });
} else {
entry.generic = Math.round((entry.generic + it.amount) * 1000) / 1000;
}
pantry[it.ingredientId] = recalcTotal(entry);
} else {
const cur = typeof val === 'number' ? val : 0;
// Generic ingredient (no products)
const cur = typeof pantry[it.ingredientId] === 'number' ? pantry[it.ingredientId] : 0;
pantry[it.ingredientId] = Math.round((cur + it.amount) * 1000) / 1000;
}
}

View File

@@ -15,7 +15,7 @@ import {
savePlans,
} from '../services/planStore.js?v=2';
import { dayHasAnyMeal, autoSelectProducts, saveLastProductSelection } from '../services/planIngredients.js?v=4';
import { loadPantry } from '../services/pantryShopping.js?v=2';
import { loadPantry, getPantryTotal, getPantryProducts, setPantryQty, setPantryProductQty, addOrMergeShoppingLines } from '../services/pantryShopping.js?v=2';
import { showAppToast } from './toast.js';
import {
bindCalendarDayClicks,
@@ -80,6 +80,8 @@ function getProductCardHTML() {
</div>
</div>
</div>
<div id="mpe-pc-stock" class="space-y-1.5"></div>
<div id="mpe-pc-shop"></div>
</div>
</div>
</div>`;
@@ -131,10 +133,100 @@ function openProductCard(ingredientId, productId) {
document.getElementById('mpe-pc-carbs').textContent = nutrition.carbs + 'g';
}
// Stock section
renderPlannerCardStock(ingredientId, productId);
// Shop section
renderPlannerCardShop(ingredientId, productId);
overlay.classList.remove('hidden');
overlay.style.pointerEvents = 'auto';
}
function renderPlannerCardStock(ingredientId, productId) {
const wrap = document.getElementById('mpe-pc-stock');
if (!wrap) return;
const def = INGREDIENTS[ingredientId];
if (!def) return;
const u = def.pantryUnit === 'szt' ? 'szt.' : def.pantryUnit;
const pantry = loadPantry();
const product = productId ? PRODUCTS[productId] : null;
let qty, step, pid;
if (product) {
const items = getPantryProducts(ingredientId, pantry);
qty = items.find(i => i.productId === productId)?.qty || 0;
step = product.packSize || 1;
pid = productId;
} else {
qty = getPantryTotal(ingredientId, pantry);
step = def.purchasePack?.amount || (def.pantryUnit === 'szt' ? 1 : 10);
pid = '_generic';
}
wrap.innerHTML = `
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5 text-gray-500">Zapas</p>
<div class="flex items-center justify-center gap-3 rounded-xl px-3 py-2 bg-[#393937]">
<button type="button" class="mpe-pc-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95 bg-[#2f2f2d] text-[#d7d2c8]" data-pid="${esc(pid)}" data-step="${step}" data-dir="-1"><i class="fas fa-minus text-xs"></i></button>
<span class="text-[17px] font-bold tabular-nums" style="color:#6ee7b7;">${Math.round(qty)} ${esc(u)}</span>
<button type="button" class="mpe-pc-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95 bg-[#2f2f2d] text-[#d7d2c8]" data-pid="${esc(pid)}" data-step="${step}" data-dir="1"><i class="fas fa-plus text-xs"></i></button>
</div>`;
wrap.querySelectorAll('.mpe-pc-stock-btn').forEach(btn => {
btn.addEventListener('click', () => {
const bpid = btn.dataset.pid;
const bstep = Number(btn.dataset.step) || 1;
const dir = Number(btn.dataset.dir);
const p = loadPantry();
if (bpid === '_generic') {
const cur = getPantryTotal(ingredientId, p);
setPantryQty(ingredientId, Math.max(0, cur + bstep * dir));
} else {
const items = getPantryProducts(ingredientId, p);
const cur = items.find(i => i.productId === bpid)?.qty || 0;
setPantryProductQty(ingredientId, bpid, Math.max(0, cur + bstep * dir));
}
renderPlannerCardStock(ingredientId, productId);
});
});
}
function renderPlannerCardShop(ingredientId, productId) {
const wrap = document.getElementById('mpe-pc-shop');
if (!wrap) return;
const def = INGREDIENTS[ingredientId];
if (!def) return;
const u = def.pantryUnit === 'szt' ? 'szt.' : def.pantryUnit;
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ę';
wrap.innerHTML = `
<button type="button" id="mpe-pc-add-list" class="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-[13px] font-semibold transition-colors active:scale-[0.98]" style="background:#ddd6ca; color:#2d2e2b;">
<i class="fas fa-cart-plus text-[11px]"></i>${esc(btnLabel)}
</button>`;
document.getElementById('mpe-pc-add-list')?.addEventListener('click', () => {
const amt = usesPacks ? packSize : (def.pantryUnit === 'szt' ? 1 : 10);
const line = {
ingredientId,
amount: amt,
unit: u,
name: product?.name || def.name,
category: def.category,
sourceNote: packLabel || 'Z planera',
};
if (productId) line.productId = productId;
addOrMergeShoppingLines([line]);
// Quick visual feedback
const btn = document.getElementById('mpe-pc-add-list');
if (btn) { btn.textContent = '✓ Dodano'; setTimeout(() => { btn.innerHTML = `<i class="fas fa-cart-plus text-[11px]"></i>${esc(btnLabel)}`; }, 1200); }
window.refreshShopping?.();
});
}
function closeProductCard() {
const overlay = document.getElementById('mpe-product-card-overlay');
if (overlay) {

View File

@@ -6,7 +6,7 @@ import {
getProductsForIngredient,
ingredientHasProducts,
} from '../data/catalog.js?v=8';
import { addIngredientToKitchenList, categoryLabel, loadPantry, setPantryQty, setPantryProductQty, getPantryTotal, getPantryProducts, getPantryGeneric } from '../services/pantryShopping.js?v=2';
import { addIngredientToKitchenList, addOrMergeShoppingLines, categoryLabel, loadPantry, setPantryQty, setPantryProductQty, getPantryTotal, getPantryProducts } from '../services/pantryShopping.js?v=2';
import { showAppToast } from '../ui/toast.js';
/* ── helpers ── */
@@ -225,35 +225,67 @@ function getFilteredIds(searchRaw) {
}).sort((a, b) => INGREDIENTS[a].name.localeCompare(INGREDIENTS[b].name, 'pl'));
}
function rowHtml(id, pantry) {
function ingredientRowHtml(id, pantry) {
const def = INGREDIENTS[id];
const qty = getPantryTotal(id, pantry);
const u = unitLabel(def.pantryUnit);
const hasStock = qty > 0;
const icon = CATEGORY_ICONS[def.category] || 'fa-jar';
const products = getProductsForIngredient(id);
const productCount = products.length;
const icon = CATEGORY_ICONS[def.category] || 'fa-jar';
const u = unitLabel(def.pantryUnit);
if (products.length === 0) {
// Simple row — no products
const qty = getPantryTotal(id, pantry);
const qtyColor = qty > 0 ? '#6ee7b7' : '#6d6c67';
const avatar = def.image
? `<img src="${esc(def.image)}" alt="" class="w-10 h-10 rounded-xl object-cover shrink-0">`
: `<div class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0" style="background:#2f2f2d;"><i class="fas ${icon} text-sm" style="color:#6d6c67;"></i></div>`;
const qtyColor = hasStock ? '#6ee7b7' : '#6d6c67';
const qtyText = hasStock ? `${Math.round(qty)} ${esc(u)}` : `0 ${esc(u)}`;
let meta = esc(categoryLabel(def.category));
if (productCount > 0) meta += ` · ${productCount} ${productCount === 1 ? 'produkt' : productCount < 5 ? 'produkty' : 'produktów'}`;
return `<button type="button" class="pv2-chip w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-left transition-colors active:scale-[0.99]" style="background:#393937;" data-id="${esc(id)}">
${avatar}
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-2">
<span class="text-[13px] font-semibold truncate" style="color:#ddd6ca;">${esc(def.name)}</span>
<span class="text-[13px] font-bold tabular-nums shrink-0" style="color:${qtyColor};">${qtyText}</span>
<span class="text-[13px] font-bold tabular-nums shrink-0" style="color:${qtyColor};">${qty > 0 ? Math.round(qty) : 0} ${esc(u)}</span>
</div>
<span class="text-[11px] block mt-0.5" style="color:#9b978f;">${meta}</span>
<span class="text-[11px] block mt-0.5" style="color:#9b978f;">${esc(categoryLabel(def.category))}</span>
</div>
</button>`;
}
// Group — ingredient header + product rows
const totalQty = getPantryTotal(id, pantry);
const pantryItems = getPantryProducts(id, pantry);
const totalColor = totalQty > 0 ? '#6ee7b7' : '#6d6c67';
let html = `<div class="rounded-xl overflow-hidden" style="background:#393937;">`;
// Group header
html += `<div class="flex items-center gap-3 px-3 py-2">`;
const avatar = def.image
? `<img src="${esc(def.image)}" alt="" class="w-8 h-8 rounded-lg object-cover shrink-0">`
: `<div class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0" style="background:#2f2f2d;"><i class="fas ${icon} text-xs" style="color:#6d6c67;"></i></div>`;
html += avatar;
html += `<div class="flex-1 min-w-0">
<span class="text-[12px] font-semibold truncate block" style="color:#9b978f;">${esc(def.name)}</span>
</div>`;
html += `<span class="text-[12px] font-bold tabular-nums shrink-0" style="color:${totalColor};">${totalQty > 0 ? Math.round(totalQty) : 0} ${esc(u)}</span>`;
html += `</div>`;
// Product rows
for (const p of products) {
const pQty = pantryItems.find(i => i.productId === p.id)?.qty || 0;
const pQtyColor = pQty > 0 ? '#ddd6ca' : '#6d6c67';
const pAvatar = p.image
? `<img src="${esc(p.image)}" alt="" class="w-8 h-8 rounded-lg object-cover shrink-0">`
: `<div class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0" style="background:#2f2f2d;"><i class="fas ${icon} text-xs" style="color:#6d6c67;"></i></div>`;
html += `<button type="button" class="pv2-product-row w-full flex items-center gap-3 px-3 py-2 text-left transition-colors active:scale-[0.99]" style="border-top:1px solid #444442;" data-id="${esc(id)}" data-product-id="${esc(p.id)}">
${pAvatar}
<div class="flex-1 min-w-0">
<span class="text-[13px] font-semibold truncate block" style="color:#ddd6ca;">${esc(p.name)}</span>
<span class="text-[11px]" style="color:#9b978f;">${p.packLabel || ''}</span>
</div>
<span class="text-[13px] font-bold tabular-nums shrink-0" style="color:${pQtyColor};">${Math.round(pQty)} ${esc(u)}</span>
</button>`;
}
html += `</div>`;
return html;
}
function groupByCategory(ids) {
@@ -302,14 +334,17 @@ function renderBoard() {
<p class="text-[10px] font-bold uppercase tracking-wider mb-2 px-0.5" style="color:#9b978f;">
<i class="fas ${icon} text-[10px] mr-1"></i>${esc(categoryLabel(cat))}
</p>
<div class="space-y-2">${ids.map(id => rowHtml(id, pantry)).join('')}</div>
<div class="space-y-2">${ids.map(id => ingredientRowHtml(id, pantry)).join('')}</div>
</div>`;
}
root.innerHTML = html;
root.querySelectorAll('.pv2-chip').forEach(btn => {
btn.addEventListener('click', () => openIngredientCard(btn.dataset.id));
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));
});
}
@@ -318,46 +353,51 @@ function renderBoard() {
/* ══════════════════════ INGREDIENT CARD ══════════════════════ */
function openIngredientCard(ingredientId) {
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 qty = getPantryTotal(ingredientId, pantry);
const u = unitLabel(def.pantryUnit);
const icon = CATEGORY_ICONS[def.category] || 'fa-jar';
// Hero image
// 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 (def.image) {
img.src = def.image; img.alt = def.name; img.classList.remove('hidden'); fallback.classList.add('hidden');
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
document.getElementById('pv2-card-category').textContent = categoryLabel(def.category);
document.getElementById('pv2-card-name').textContent = def.name;
// 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');
if (def.purchasePack) {
packEl.textContent = def.purchasePack.label || `${def.purchasePack.amount} ${u}`;
const packLabel = product?.packLabel || def.purchasePack?.label;
if (packLabel) {
packEl.textContent = packLabel;
packEl.classList.remove('hidden');
} else {
packEl.classList.add('hidden');
}
// Nutrition
renderCardNutrition(def);
// Nutrition — use product values if available
renderCardNutrition(def, product);
// Stock
renderCardStock(ingredientId, pantry);
renderCardStock(ingredientId, editingProductId, pantry);
// Shopping
renderCardShop(ingredientId);
renderCardShop(ingredientId, editingProductId);
// Show
const overlay = document.getElementById('pv2-card-overlay');
@@ -371,10 +411,10 @@ function closeIngredientCard() {
renderBoard();
}
function renderCardNutrition(def) {
function renderCardNutrition(def, product) {
const wrap = document.getElementById('pv2-card-nutrition');
if (!wrap) return;
const n = def.nutritionPer100g;
const n = product?.nutritionPer100g || def.nutritionPer100g;
if (!n) { wrap.innerHTML = ''; return; }
const label = def.pantryUnit === 'ml' ? 'na 100 ml' : 'na 100 g';
wrap.innerHTML = `
@@ -399,43 +439,29 @@ function renderCardNutrition(def) {
</div>`;
}
function renderCardStock(ingredientId, pantry) {
function renderCardStock(ingredientId, productId, pantry) {
const wrap = document.getElementById('pv2-card-stock-section');
if (!wrap) return;
const def = INGREDIENTS[ingredientId];
if (!def) return;
const u = unitLabel(def.pantryUnit);
const qty = getPantryTotal(ingredientId, pantry);
const products = getProductsForIngredient(ingredientId);
const hasProds = products.length > 0;
let html = `<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p>`;
if (hasProds) {
const pantryProducts = getPantryProducts(ingredientId, pantry);
const generic = getPantryGeneric(ingredientId, pantry);
const productQty = (pid) => pantryProducts.find(i => i.productId === pid)?.qty || 0;
html += `<div class="rounded-xl px-3 py-2 space-y-1.5" style="background:#393937;">`;
html += `<div class="flex items-center justify-between"><span class="text-[12px] font-semibold" style="color:#9b978f;">Łącznie</span><span class="text-[13px] font-bold tabular-nums" style="color:#6ee7b7;">${Math.round(qty)} ${esc(u)}</span></div>`;
html += `<div style="border-top:1px solid #444442; padding-top:0.375rem;" class="space-y-1">`;
for (const p of products) {
const q = Math.round(productQty(p.id));
html += `<div class="flex items-center gap-1.5">
<span class="flex-1 text-[12px] truncate" style="color:#d7d2c8;">${esc(p.name)}</span>
<button type="button" class="pv2-stock-btn w-7 h-7 rounded-lg flex items-center justify-center active:scale-95 shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-pid="${esc(p.id)}" data-step="${p.packSize}" data-dir="-1"><i class="fas fa-minus text-[9px]"></i></button>
<span class="w-12 text-center text-[12px] font-semibold tabular-nums" style="color:#ddd6ca;">${q} ${esc(u)}</span>
<button type="button" class="pv2-stock-btn w-7 h-7 rounded-lg flex items-center justify-center active:scale-95 shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-pid="${esc(p.id)}" data-step="${p.packSize}" data-dir="1"><i class="fas fa-plus text-[9px]"></i></button>
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 += `<div class="flex items-center justify-center gap-3 rounded-xl px-3 py-2" style="background:#393937;">
<button type="button" class="pv2-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95" style="background:#2f2f2d; color:#d7d2c8;" data-pid="${esc(productId)}" data-step="${step}" data-dir="-1"><i class="fas fa-minus text-xs"></i></button>
<span class="text-[17px] font-bold tabular-nums" style="color:#6ee7b7;">${Math.round(qty)} ${esc(u)}</span>
<button type="button" class="pv2-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95" style="background:#2f2f2d; color:#d7d2c8;" data-pid="${esc(productId)}" data-step="${step}" data-dir="1"><i class="fas fa-plus text-xs"></i></button>
</div>`;
}
html += `<div class="flex items-center gap-1.5">
<span class="flex-1 text-[12px] italic truncate" style="color:#9b978f;">Nieokreślony</span>
<button type="button" class="pv2-stock-btn w-7 h-7 rounded-lg flex items-center justify-center active:scale-95 shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-pid="_generic" data-step="${pantryQtyStep(ingredientId)}" data-dir="-1"><i class="fas fa-minus text-[9px]"></i></button>
<span class="w-12 text-center text-[12px] font-semibold tabular-nums" style="color:#ddd6ca;">${Math.round(generic)} ${esc(u)}</span>
<button type="button" class="pv2-stock-btn w-7 h-7 rounded-lg flex items-center justify-center active:scale-95 shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-pid="_generic" data-step="${pantryQtyStep(ingredientId)}" data-dir="1"><i class="fas fa-plus text-[9px]"></i></button>
</div>`;
html += `</div></div>`;
} else {
// Generic ingredient — simple +/-
const qty = getPantryTotal(ingredientId, pantry);
const step = pantryQtyStep(ingredientId);
html += `<div class="flex items-center justify-center gap-3 rounded-xl px-3 py-2" style="background:#393937;">
<button type="button" class="pv2-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95" style="background:#2f2f2d; color:#d7d2c8;" data-pid="_generic" data-step="${step}" data-dir="-1"><i class="fas fa-minus text-xs"></i></button>
@@ -446,7 +472,6 @@ function renderCardStock(ingredientId, pantry) {
wrap.innerHTML = html;
// Bind stock buttons
wrap.querySelectorAll('.pv2-stock-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (!editingId) return;
@@ -455,48 +480,56 @@ function renderCardStock(ingredientId, pantry) {
const dir = Number(btn.dataset.dir);
const p = loadPantry();
if (pid === '_generic') {
const cur = getPantryGeneric(editingId, p);
const cur = getPantryTotal(editingId, p);
setPantryQty(editingId, Math.max(0, cur + step * 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));
}
renderCardStock(editingId, loadPantry());
renderCardStock(editingId, editingProductId, loadPantry());
});
});
}
function renderCardShop(ingredientId) {
function renderCardShop(ingredientId, productId) {
const wrap = document.getElementById('pv2-card-shop-section');
if (!wrap) return;
const def = INGREDIENTS[ingredientId];
if (!def) return;
const u = unitLabel(def.pantryUnit);
const pack = def.purchasePack;
const usesPacks = Boolean(pack && pack.amount > 0);
const hint = usesPacks ? `1 opak. = ${pack.label || `${pack.amount} ${u}`}` : '';
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ę';
wrap.innerHTML = `
<button type="button" id="pv2-card-add-list" class="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-[13px] font-semibold transition-colors active:scale-[0.98]" style="background:#ddd6ca; color:#2d2e2b;">
<i class="fas fa-cart-plus text-[11px]"></i>Dodaj na listę${usesPacks ? ' (1 opak.)' : ''}
</button>
${hint ? `<p class="text-[10px] text-center mt-1" style="color:#9b978f;">${esc(hint)}</p>` : ''}`;
<i class="fas fa-cart-plus text-[11px]"></i>${esc(btnLabel)}
</button>`;
document.getElementById('pv2-card-add-list')?.addEventListener('click', () => {
if (!editingId) return;
const d = INGREDIENTS[editingId];
if (!d) return;
const uLabel = unitLabel(d.pantryUnit);
if (usesPacks && d.purchasePack) {
const amt = d.purchasePack.amount;
addIngredientToKitchenList(editingId, amt, d.purchasePack.label || `${amt} ${uLabel}`);
showAppToast(`Dodano 1 opak. na listę.`);
} else {
const step = pantryQtyStep(editingId);
addIngredientToKitchenList(editingId, step);
showAppToast(`Dodano ${step} ${uLabel} na listę.`);
}
const amt = usesPacks ? packSize : pantryQtyStep(editingId);
const note = usesPacks ? (packLabel || `${packSize} ${uLabel}`) : undefined;
// Use addOrMergeShoppingLines to include productId
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?.();
});
}