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

@@ -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);
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>
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>`;
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};">${qty > 0 ? Math.round(qty) : 0} ${esc(u)}</span>
</div>
<span class="text-[11px] block mt-0.5" style="color:#9b978f;">${esc(categoryLabel(def.category))}</span>
</div>
<span class="text-[11px] block mt-0.5" style="color:#9b978f;">${meta}</span>
</div>
</button>`;
</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>
</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>
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></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?.();
});
}