This commit is contained in:
@@ -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?.();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user