Unify product cards and edit packaged amounts by pack
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m13s

This commit is contained in:
2026-04-10 15:29:52 +02:00
parent 86d47f126d
commit 527324515a
3 changed files with 101 additions and 22 deletions

View File

@@ -107,6 +107,47 @@ function formatPackAwareAmount(amount, pantryUnit, packSize, packLabel) {
return `${formatQty(qty)} ${unit}`; return `${formatQty(qty)} ${unit}`;
} }
function normalizeQty(value) {
return Math.max(0, Math.round((Number(value) || 0) * 100) / 100);
}
function formatPreciseQty(n) {
const rounded = Math.round((Number(n) || 0) * 1000) / 1000;
if (Number.isInteger(rounded)) return String(rounded);
return rounded.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
}
function formatPackCount(amount, packSize) {
if (!Number.isFinite(Number(packSize)) || Number(packSize) <= 0) return '';
return `${formatPreciseQty((Number(amount) || 0) / Number(packSize))} opak.`;
}
function getQtyStepMeta(def, product = null) {
const productPackSize = Number(product?.packSize);
if (Number.isFinite(productPackSize) && productPackSize > 0) {
return {
step: productPackSize,
usesPackStep: true,
stepLabel: product?.packLabel || formatQtyWithUnit(productPackSize, def.pantryUnit),
};
}
const ingredientPackSize = Number(def?.purchasePack?.amount);
if (Number.isFinite(ingredientPackSize) && ingredientPackSize > 0) {
return {
step: ingredientPackSize,
usesPackStep: true,
stepLabel: def.purchasePack?.label || formatQtyWithUnit(ingredientPackSize, def.pantryUnit),
};
}
return {
step: pantryQtyStep(def.id),
usesPackStep: false,
stepLabel: '',
};
}
export function getIngredientCardHTML({ export function getIngredientCardHTML({
idBase, idBase,
overlayClass = 'fixed inset-0 z-[70] hidden opacity-0 transition-opacity duration-200 flex items-center justify-center p-5', overlayClass = 'fixed inset-0 z-[70] hidden opacity-0 transition-opacity duration-200 flex items-center justify-center p-5',
@@ -153,6 +194,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
ingredientId: null, ingredientId: null,
productId: null, productId: null,
selectedProductId: null, selectedProductId: null,
allowProductSelection: true,
sourceNote: defaultSourceNote, sourceNote: defaultSourceNote,
onProductChange: null, onProductChange: null,
onAfterChange: null, onAfterChange: null,
@@ -234,7 +276,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
} }
if (backBtn) { if (backBtn) {
backBtn.classList.toggle('hidden', !(hasProducts && state.productId)); backBtn.classList.toggle('hidden', !(hasProducts && state.productId && state.allowProductSelection));
} }
} }
@@ -250,7 +292,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
const hasProducts = ingredientHasProducts(def.id); const hasProducts = ingredientHasProducts(def.id);
const unitScope = def.pantryUnit === 'ml' ? 'na 100 ml' : 'na 100 g'; const unitScope = def.pantryUnit === 'ml' ? 'na 100 ml' : 'na 100 g';
const hint = product const hint = product
? 'dokładne dla produktu' ? ''
: hasProducts : hasProducts
? 'orientacyjnie dla składnika' ? 'orientacyjnie dla składnika'
: ''; : '';
@@ -311,12 +353,20 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
const qty = product const qty = product
? (getPantryProducts(state.ingredientId, pantry).find((i) => i.productId === state.productId)?.qty || 0) ? (getPantryProducts(state.ingredientId, pantry).find((i) => i.productId === state.productId)?.qty || 0)
: totalQty; : totalQty;
const step = product ? (product.packSize || pantryQtyStep(state.ingredientId)) : pantryQtyStep(state.ingredientId); const { step, usesPackStep } = getQtyStepMeta(def, product);
const packSize = product?.packSize || def.purchasePack?.amount || 0; const packSize = product?.packSize || def.purchasePack?.amount || 0;
const packLabel = product?.packLabel || def.purchasePack?.label || ''; const packLabel = product?.packLabel || def.purchasePack?.label || '';
const draftQty = state.stockEditorOpen const draftQty = state.stockEditorOpen
? Math.max(0, Number(state.stockDraftQty ?? qty) || 0) ? normalizeQty(state.stockDraftQty ?? qty)
: qty; : qty;
const stockValueLabel = usesPackStep
? formatPackCount(qty, step)
: formatPackAwareAmount(qty, def.pantryUnit, packSize, packLabel);
const stockSubLabel = usesPackStep ? formatQtyWithUnit(qty, def.pantryUnit) : '';
const draftInputValue = usesPackStep
? formatPreciseQty(draftQty / step)
: formatPreciseQty(draftQty);
const draftInputUnit = usesPackStep ? 'opak.' : unit;
const actionLabel = state.stockEditorOpen ? 'Anuluj' : 'Zmień'; const actionLabel = state.stockEditorOpen ? 'Anuluj' : 'Zmień';
wrap.innerHTML = ` wrap.innerHTML = `
@@ -324,7 +374,8 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
<div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;"> <div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-[16px] font-bold tabular-nums" style="color:#6ee7b7;">${esc(formatPackAwareAmount(qty, def.pantryUnit, packSize, packLabel))}</p> <p class="text-[16px] font-bold tabular-nums" style="color:#6ee7b7;">${esc(stockValueLabel)}</p>
${stockSubLabel ? `<p class="text-[11px] mt-1" style="color:#9b978f;">${esc(stockSubLabel)}</p>` : ''}
</div> </div>
<button type="button" class="ingredient-card-stock-toggle inline-flex items-center rounded-full px-2.5 py-1 text-[10px] font-semibold shrink-0" style="background:${state.stockEditorOpen ? '#23221e' : '#2f2f2d'}; color:${state.stockEditorOpen ? '#f2efe8' : '#d7d2c8'};"> <button type="button" class="ingredient-card-stock-toggle inline-flex items-center rounded-full px-2.5 py-1 text-[10px] font-semibold shrink-0" style="background:${state.stockEditorOpen ? '#23221e' : '#2f2f2d'}; color:${state.stockEditorOpen ? '#f2efe8' : '#d7d2c8'};">
${esc(actionLabel)} ${esc(actionLabel)}
@@ -337,13 +388,14 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
<i class="fas fa-minus text-xs"></i> <i class="fas fa-minus text-xs"></i>
</button> </button>
<label class="flex-1 rounded-xl px-3 py-2 flex items-center justify-center gap-2" style="background:#2f2f2d;"> <label class="flex-1 rounded-xl px-3 py-2 flex items-center justify-center gap-2" style="background:#2f2f2d;">
<input type="number" min="0" step="${step}" value="${formatQty(draftQty)}" class="ingredient-card-stock-input w-20 bg-transparent text-center text-[14px] font-semibold tabular-nums outline-none appearance-none" style="color:#ddd6ca; background:transparent !important; border:none !important; box-shadow:none !important; -webkit-appearance:none; -moz-appearance:textfield;"> <input type="number" min="0" step="${usesPackStep ? '1' : step}" value="${draftInputValue}" class="ingredient-card-stock-input w-20 bg-transparent text-center text-[14px] font-semibold tabular-nums outline-none appearance-none" style="color:#ddd6ca; background:transparent !important; border:none !important; box-shadow:none !important; -webkit-appearance:none; -moz-appearance:textfield;">
<span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(unit)}</span> <span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(draftInputUnit)}</span>
</label> </label>
<button type="button" class="ingredient-card-stock-step w-9 h-9 rounded-xl flex items-center justify-center shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-dir="1" aria-label="Zwiększ szkic zapasu"> <button type="button" class="ingredient-card-stock-step w-9 h-9 rounded-xl flex items-center justify-center shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-dir="1" aria-label="Zwiększ szkic zapasu">
<i class="fas fa-plus text-xs"></i> <i class="fas fa-plus text-xs"></i>
</button> </button>
</div> </div>
${usesPackStep ? `<p class="text-[10px] mt-2 text-right" style="color:#9b978f;">${esc(formatQtyWithUnit(draftQty, def.pantryUnit))}</p>` : ''}
<div class="flex items-center justify-between gap-3 mt-3"> <div class="flex items-center justify-between gap-3 mt-3">
<button type="button" class="ingredient-card-stock-clear text-[11px] font-semibold" style="color:#9b978f;">Wyzeruj</button> <button type="button" class="ingredient-card-stock-clear text-[11px] font-semibold" style="color:#9b978f;">Wyzeruj</button>
<button type="button" class="ingredient-card-stock-save inline-flex items-center rounded-full px-3 py-1.5 text-[11px] font-semibold" style="background:#ddd6ca; color:#2d2e2b;">Zapisz</button> <button type="button" class="ingredient-card-stock-save inline-flex items-center rounded-full px-3 py-1.5 text-[11px] font-semibold" style="background:#ddd6ca; color:#2d2e2b;">Zapisz</button>
@@ -366,14 +418,16 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
wrap.querySelectorAll('.ingredient-card-stock-step').forEach((btn) => { wrap.querySelectorAll('.ingredient-card-stock-step').forEach((btn) => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const dir = Number(btn.dataset.dir) || 1; const dir = Number(btn.dataset.dir) || 1;
const next = Math.max(0, Math.round(((Number(state.stockDraftQty ?? qty) || 0) + step * dir) * 100) / 100); const next = normalizeQty((Number(state.stockDraftQty ?? qty) || 0) + step * dir);
state.stockDraftQty = next; state.stockDraftQty = next;
render(); render();
}); });
}); });
wrap.querySelector('.ingredient-card-stock-input')?.addEventListener('input', (event) => { wrap.querySelector('.ingredient-card-stock-input')?.addEventListener('input', (event) => {
state.stockDraftQty = Math.max(0, Number(event.target.value) || 0); state.stockDraftQty = usesPackStep
? normalizeQty((Number(event.target.value) || 0) * step)
: normalizeQty(event.target.value);
}); });
wrap.querySelector('.ingredient-card-stock-clear')?.addEventListener('click', () => { wrap.querySelector('.ingredient-card-stock-clear')?.addEventListener('click', () => {
@@ -383,7 +437,9 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
wrap.querySelector('.ingredient-card-stock-save')?.addEventListener('click', () => { wrap.querySelector('.ingredient-card-stock-save')?.addEventListener('click', () => {
const input = wrap.querySelector('.ingredient-card-stock-input'); const input = wrap.querySelector('.ingredient-card-stock-input');
const nextQty = Math.max(0, Math.round((Number(input?.value ?? state.stockDraftQty ?? qty) || 0) * 100) / 100); const nextQty = usesPackStep
? normalizeQty((Number(input?.value) || 0) * step)
: normalizeQty(input?.value ?? state.stockDraftQty ?? qty);
if (state.productId) { if (state.productId) {
setPantryProductQty(state.ingredientId, state.productId, nextQty); setPantryProductQty(state.ingredientId, state.productId, nextQty);
} else { } else {
@@ -421,7 +477,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
function renderProducts() { function renderProducts() {
const wrap = el('products'); const wrap = el('products');
if (!wrap || !state.ingredientId) return; if (!wrap || !state.ingredientId) return;
if (!ingredientHasProducts(state.ingredientId)) { if (!ingredientHasProducts(state.ingredientId) || !state.allowProductSelection) {
wrap.innerHTML = ''; wrap.innerHTML = '';
return; return;
} }
@@ -459,18 +515,30 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
const def = INGREDIENTS[state.ingredientId]; const def = INGREDIENTS[state.ingredientId];
if (!def) return; if (!def) return;
const hasProducts = ingredientHasProducts(state.ingredientId);
const product = state.productId ? PRODUCTS[state.productId] : null; const product = state.productId ? PRODUCTS[state.productId] : null;
const { step, usesPackStep } = getQtyStepMeta(def, product);
const packSize = product?.packSize || def.purchasePack?.amount; const packSize = product?.packSize || def.purchasePack?.amount;
const packLabel = product?.packLabel || def.purchasePack?.label; const packLabel = product?.packLabel || def.purchasePack?.label;
const usesPacks = Boolean(packSize && packSize > 0); const usesPacks = Boolean(packSize && packSize > 0);
const defaultAmount = usesPacks ? packSize : pantryQtyStep(state.ingredientId); const defaultAmount = step;
const shoppingItem = getCurrentShoppingItem(def); const shoppingItem = getCurrentShoppingItem(def);
const hasShoppingItem = Boolean(shoppingItem); const hasShoppingItem = Boolean(shoppingItem);
const shoppingAmount = shoppingItem?.amount || 0; const shoppingAmount = shoppingItem?.amount || 0;
const draftQty = state.shopEditorOpen const draftQty = state.shopEditorOpen
? Math.max(0, Number(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) || 0) ? normalizeQty(state.shopDraftQty ?? (shoppingAmount || defaultAmount))
: shoppingAmount; : shoppingAmount;
const shopValueLabel = hasShoppingItem
? usesPackStep
? formatPackCount(shoppingAmount, step)
: formatPackAwareAmount(shoppingAmount, def.pantryUnit, packSize, packLabel)
: 'Brak na liście';
const shopSubLabel = hasShoppingItem && usesPackStep
? formatQtyWithUnit(shoppingAmount, def.pantryUnit)
: '';
const shopInputValue = usesPackStep
? formatPreciseQty(draftQty / step)
: formatPreciseQty(draftQty);
const shopInputUnit = usesPackStep ? 'opak.' : unitLabel(def.pantryUnit);
const actionLabel = state.shopEditorOpen ? 'Anuluj' : 'Zmień'; const actionLabel = state.shopEditorOpen ? 'Anuluj' : 'Zmień';
wrap.innerHTML = ` wrap.innerHTML = `
@@ -478,7 +546,8 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
<div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;"> <div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-[16px] font-bold tabular-nums" style="color:${hasShoppingItem ? '#ddd6ca' : '#9b978f'};">${esc(hasShoppingItem ? formatPackAwareAmount(shoppingAmount, def.pantryUnit, packSize, packLabel) : 'Brak na liscie')}</p> <p class="text-[16px] font-bold tabular-nums" style="color:${hasShoppingItem ? '#ddd6ca' : '#9b978f'};">${esc(shopValueLabel)}</p>
${shopSubLabel ? `<p class="text-[11px] mt-1" style="color:#9b978f;">${esc(shopSubLabel)}</p>` : ''}
</div> </div>
<button type="button" class="ingredient-card-shop-toggle inline-flex items-center rounded-full px-2.5 py-1 text-[10px] font-semibold shrink-0" style="background:${state.shopEditorOpen ? '#23221e' : '#2f2f2d'}; color:${state.shopEditorOpen ? '#f2efe8' : '#d7d2c8'};"> <button type="button" class="ingredient-card-shop-toggle inline-flex items-center rounded-full px-2.5 py-1 text-[10px] font-semibold shrink-0" style="background:${state.shopEditorOpen ? '#23221e' : '#2f2f2d'}; color:${state.shopEditorOpen ? '#f2efe8' : '#d7d2c8'};">
${esc(actionLabel)} ${esc(actionLabel)}
@@ -491,13 +560,14 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
<i class="fas fa-minus text-xs"></i> <i class="fas fa-minus text-xs"></i>
</button> </button>
<label class="flex-1 rounded-xl px-3 py-2 flex items-center justify-center gap-2" style="background:#2f2f2d;"> <label class="flex-1 rounded-xl px-3 py-2 flex items-center justify-center gap-2" style="background:#2f2f2d;">
<input type="number" min="0" step="${defaultAmount}" value="${formatQty(draftQty)}" class="ingredient-card-shop-input w-20 bg-transparent text-center text-[14px] font-semibold tabular-nums outline-none appearance-none" style="color:#ddd6ca; background:transparent !important; border:none !important; box-shadow:none !important; -webkit-appearance:none; -moz-appearance:textfield;"> <input type="number" min="0" step="${usesPackStep ? '1' : defaultAmount}" value="${shopInputValue}" class="ingredient-card-shop-input w-20 bg-transparent text-center text-[14px] font-semibold tabular-nums outline-none appearance-none" style="color:#ddd6ca; background:transparent !important; border:none !important; box-shadow:none !important; -webkit-appearance:none; -moz-appearance:textfield;">
<span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(unitLabel(def.pantryUnit))}</span> <span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(shopInputUnit)}</span>
</label> </label>
<button type="button" class="ingredient-card-shop-step w-9 h-9 rounded-xl flex items-center justify-center shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-dir="1" aria-label="Zwiększ ilość na liście"> <button type="button" class="ingredient-card-shop-step w-9 h-9 rounded-xl flex items-center justify-center shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-dir="1" aria-label="Zwiększ ilość na liście">
<i class="fas fa-plus text-xs"></i> <i class="fas fa-plus text-xs"></i>
</button> </button>
</div> </div>
${usesPackStep ? `<p class="text-[10px] mt-2 text-right" style="color:#9b978f;">${esc(formatQtyWithUnit(draftQty, def.pantryUnit))}</p>` : ''}
<div class="flex items-center justify-between gap-3 mt-3"> <div class="flex items-center justify-between gap-3 mt-3">
${hasShoppingItem ${hasShoppingItem
? '<button type="button" class="ingredient-card-shop-remove text-[11px] font-semibold" style="color:#9b978f;">Usuń z listy</button>' ? '<button type="button" class="ingredient-card-shop-remove text-[11px] font-semibold" style="color:#9b978f;">Usuń z listy</button>'
@@ -522,14 +592,16 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
wrap.querySelectorAll('.ingredient-card-shop-step').forEach((btn) => { wrap.querySelectorAll('.ingredient-card-shop-step').forEach((btn) => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const dir = Number(btn.dataset.dir) || 1; const dir = Number(btn.dataset.dir) || 1;
const next = Math.max(0, Math.round(((Number(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) || 0) + defaultAmount * dir) * 100) / 100); const next = normalizeQty((Number(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) || 0) + step * dir);
state.shopDraftQty = next; state.shopDraftQty = next;
render(); render();
}); });
}); });
wrap.querySelector('.ingredient-card-shop-input')?.addEventListener('input', (event) => { wrap.querySelector('.ingredient-card-shop-input')?.addEventListener('input', (event) => {
state.shopDraftQty = Math.max(0, Number(event.target.value) || 0); state.shopDraftQty = usesPackStep
? normalizeQty((Number(event.target.value) || 0) * step)
: normalizeQty(event.target.value);
}); });
wrap.querySelector('.ingredient-card-shop-remove')?.addEventListener('click', () => { wrap.querySelector('.ingredient-card-shop-remove')?.addEventListener('click', () => {
@@ -545,7 +617,9 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
wrap.querySelector('.ingredient-card-shop-save')?.addEventListener('click', () => { wrap.querySelector('.ingredient-card-shop-save')?.addEventListener('click', () => {
const input = wrap.querySelector('.ingredient-card-shop-input'); const input = wrap.querySelector('.ingredient-card-shop-input');
const nextAmount = Math.max(0, Math.round((Number(input?.value ?? state.shopDraftQty ?? defaultAmount) || 0) * 100) / 100); const nextAmount = usesPackStep
? normalizeQty((Number(input?.value) || 0) * step)
: normalizeQty(input?.value ?? state.shopDraftQty ?? defaultAmount);
let toastText = null; let toastText = null;
if (shoppingItem) { if (shoppingItem) {
updateKitchenItemAmount(KITCHEN_LIST_ID, shoppingItem.id, nextAmount); updateKitchenItemAmount(KITCHEN_LIST_ID, shoppingItem.id, nextAmount);
@@ -593,6 +667,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
ingredientId, ingredientId,
productId = null, productId = null,
selectedProductId = productId, selectedProductId = productId,
allowProductSelection = true,
sourceNote = defaultSourceNote, sourceNote = defaultSourceNote,
onProductChange = null, onProductChange = null,
onAfterChange = null, onAfterChange = null,
@@ -605,6 +680,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
state.ingredientId = ingredientId; state.ingredientId = ingredientId;
state.productId = productId && PRODUCTS[productId] ? productId : null; state.productId = productId && PRODUCTS[productId] ? productId : null;
state.selectedProductId = selectedProductId && PRODUCTS[selectedProductId] ? selectedProductId : state.productId; state.selectedProductId = selectedProductId && PRODUCTS[selectedProductId] ? selectedProductId : state.productId;
state.allowProductSelection = Boolean(allowProductSelection);
state.sourceNote = sourceNote; state.sourceNote = sourceNote;
state.onProductChange = onProductChange; state.onProductChange = onProductChange;
state.onAfterChange = onAfterChange; state.onAfterChange = onAfterChange;
@@ -634,6 +710,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
state.ingredientId = null; state.ingredientId = null;
state.productId = null; state.productId = null;
state.selectedProductId = null; state.selectedProductId = null;
state.allowProductSelection = true;
state.onProductChange = null; state.onProductChange = null;
state.onAfterChange = null; state.onAfterChange = null;
state.sourceNote = defaultSourceNote; state.sourceNote = defaultSourceNote;
@@ -645,6 +722,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
bound = true; bound = true;
el('close')?.addEventListener('click', close); el('close')?.addEventListener('click', close);
el('back')?.addEventListener('click', () => { el('back')?.addEventListener('click', () => {
if (!state.allowProductSelection) return;
state.productId = null; state.productId = null;
resetInlineEditors(); resetInlineEditors();
render(); render();

View File

@@ -26,7 +26,7 @@ import {
renderCalendarGrid, renderCalendarGrid,
syncCalendarTodayButton, syncCalendarTodayButton,
} from './mealCalendar.js?v=1'; } from './mealCalendar.js?v=1';
import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-106'; import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-107';
function esc(s) { function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
@@ -674,6 +674,7 @@ export function setupMealPlanEditor() {
ingredientId: eid, ingredientId: eid,
productId: cardBtn.dataset.pid || null, productId: cardBtn.dataset.pid || null,
selectedProductId: cardBtn.dataset.pid || null, selectedProductId: cardBtn.dataset.pid || null,
allowProductSelection: !cardBtn.dataset.pid,
sourceNote: 'Z planera', sourceNote: 'Z planera',
onProductChange: (nextProductId) => { onProductChange: (nextProductId) => {
if (!nextProductId) return; if (!nextProductId) return;

View File

@@ -9,7 +9,7 @@ import {
loadPantry, loadPantry,
getPantryTotal, getPantryTotal,
} from '../services/pantryShopping.js?v=2'; } from '../services/pantryShopping.js?v=2';
import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-106'; import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-107';
/* ── helpers ── */ /* ── helpers ── */