Redesign ingredient card
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m13s

This commit is contained in:
2026-04-10 15:04:36 +02:00
parent 2883dc858e
commit 86d47f126d
4 changed files with 234 additions and 67 deletions

View File

@@ -11,7 +11,7 @@
<meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0"> <meta http-equiv="Expires" content="0">
<title>Recipe App - Modular</title> <title>Recipe App - Modular</title>
<link rel="manifest" href="./manifest.webmanifest?v=20260410-99"> <link rel="manifest" href="./manifest.webmanifest?v=20260410-106">
<link rel="icon" type="image/png" sizes="192x192" href="./icons/icon-192.png"> <link rel="icon" type="image/png" sizes="192x192" href="./icons/icon-192.png">
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png"> <link rel="apple-touch-icon" href="./icons/apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -600,7 +600,7 @@
</div> </div>
<script> <script>
const APP_ASSET_VERSION = '20260410-99'; const APP_ASSET_VERSION = '20260410-106';
const APP_VERSION_STORAGE_KEY = 'recipe-app-asset-version'; const APP_VERSION_STORAGE_KEY = 'recipe-app-asset-version';
const APP_VERSION_QUERY_KEY = 'appv'; const APP_VERSION_QUERY_KEY = 'appv';
@@ -634,7 +634,7 @@
})(); })();
</script> </script>
<script type="module"> <script type="module">
const appVersion = window.__APP_ASSET_VERSION__ || '20260410-99'; const appVersion = window.__APP_ASSET_VERSION__ || '20260410-106';
const recoveryKey = `recipe-app-recovery-${appVersion}`; const recoveryKey = `recipe-app-recovery-${appVersion}`;
function renderBootstrapError(message) { function renderBootstrapError(message) {

View File

@@ -8,11 +8,15 @@ import {
} from '../data/catalog.js?v=8'; } from '../data/catalog.js?v=8';
import { import {
addOrMergeShoppingLines, addOrMergeShoppingLines,
KITCHEN_LIST_ID,
loadPantry, loadPantry,
loadShoppingState,
removeItemFromList,
setPantryQty, setPantryQty,
setPantryProductQty, setPantryProductQty,
getPantryTotal, getPantryTotal,
getPantryProducts, getPantryProducts,
updateKitchenItemAmount,
} from '../services/pantryShopping.js?v=2'; } from '../services/pantryShopping.js?v=2';
import { showAppToast } from './toast.js'; import { showAppToast } from './toast.js';
@@ -91,6 +95,18 @@ function sortProductsByStock(products, pantryItems) {
}); });
} }
function formatPackAwareAmount(amount, pantryUnit, packSize, packLabel) {
const qty = Number(amount) || 0;
const unit = unitLabel(pantryUnit);
if (packSize && packSize > 0) {
const packs = qty / packSize;
if (qty > 0 && Number.isFinite(packs) && Math.abs(packs - Math.round(packs)) < 0.001) {
return `${formatQty(Math.round(packs))} x ${packLabel || `${formatQty(packSize)} ${unit}`}`;
}
}
return `${formatQty(qty)} ${unit}`;
}
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',
@@ -140,12 +156,36 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
sourceNote: defaultSourceNote, sourceNote: defaultSourceNote,
onProductChange: null, onProductChange: null,
onAfterChange: null, onAfterChange: null,
stockEditorOpen: false,
stockDraftQty: null,
shopEditorOpen: false,
shopDraftQty: null,
closeTimer: null, closeTimer: null,
}; };
let bound = false; let bound = false;
const el = (suffix = '') => document.getElementById(suffix ? `${idBase}-${suffix}` : idBase); const el = (suffix = '') => document.getElementById(suffix ? `${idBase}-${suffix}` : idBase);
function resetInlineEditors() {
state.stockEditorOpen = false;
state.stockDraftQty = null;
state.shopEditorOpen = false;
state.shopDraftQty = null;
}
function getCurrentShoppingItem(def) {
const shopping = loadShoppingState();
const kitchen = shopping.lists.find((list) => list.id === KITCHEN_LIST_ID && list.type === 'kitchen');
if (!kitchen || kitchen.type !== 'kitchen') return null;
const unit = unitLabel(def.pantryUnit);
return kitchen.items.find((item) => {
if (item.checked) return false;
return item.ingredientId === state.ingredientId
&& item.unit === unit
&& (item.productId || '') === (state.productId || '');
}) || null;
}
function renderHeader(def, product, pantry) { function renderHeader(def, product, pantry) {
const hasProducts = ingredientHasProducts(def.id); const hasProducts = ingredientHasProducts(def.id);
const icon = CATEGORY_ICONS[def.category] || 'fa-jar'; const icon = CATEGORY_ICONS[def.category] || 'fa-jar';
@@ -213,11 +253,12 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
? 'dokładne dla produktu' ? 'dokładne dla produktu'
: hasProducts : hasProducts
? 'orientacyjnie dla składnika' ? 'orientacyjnie dla składnika'
: 'bazowe wartości'; : '';
const nutritionMeta = hint ? `${unitScope}${hint}` : unitScope;
wrap.innerHTML = ` wrap.innerHTML = `
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Wartości odżywcze</p> <p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Wartości odżywcze</p>
<p class="text-[10px] mb-1.5" style="color:#9b978f;">${esc(unitScope)}${esc(hint)}</p> <p class="text-[10px] mb-1.5" style="color:#9b978f;">${esc(nutritionMeta)}</p>
<div class="grid grid-cols-4 gap-1.5"> <div class="grid grid-cols-4 gap-1.5">
<div class="rounded-xl px-2 py-1.5 text-center" style="background:#393937;"> <div class="rounded-xl px-2 py-1.5 text-center" style="background:#393937;">
<p class="text-[15px] font-bold tabular-nums leading-tight" style="color:#ddd6ca;">${nutrition.kcal}</p> <p class="text-[15px] font-bold tabular-nums leading-tight" style="color:#ddd6ca;">${nutrition.kcal}</p>
@@ -252,18 +293,17 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
if (hasProducts && !product) { if (hasProducts && !product) {
const stockedCount = getPantryProducts(state.ingredientId, pantry).filter((i) => i.qty > 0).length; const stockedCount = getPantryProducts(state.ingredientId, pantry).filter((i) => i.qty > 0).length;
const summaryNutrition = nutritionForQty(def, totalQty);
wrap.innerHTML = ` wrap.innerHTML = `
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p> <p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p>
<div class="rounded-xl px-3 py-2.5" style="background:#393937;"> <div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
<div class="flex items-center justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div> <div class="min-w-0 flex-1">
<p class="text-[17px] font-bold tabular-nums" style="color:#6ee7b7;">${esc(formatQty(totalQty))} ${esc(unit)}</p> <p class="text-[10px] font-semibold uppercase tracking-wide" style="color:#9b978f;">Stan łączny</p>
<p class="text-[11px] mt-0.5" style="color:#9b978f;">${stockedCount} z ${getProductsForIngredient(state.ingredientId).length} produktów ma stan</p> <p class="text-[16px] font-bold tabular-nums mt-1" style="color:#6ee7b7;">${esc(formatQty(totalQty))} ${esc(unit)}</p>
<p class="text-[11px] mt-1 leading-snug" style="color:#9b978f;">${stockedCount} z ${getProductsForIngredient(state.ingredientId).length} produktów ma zapas</p>
</div> </div>
<span class="text-[10px] text-right max-w-[92px]" style="color:#9b978f;">Wybierz produkt niżej, aby zmienić stan</span> <span class="inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold shrink-0" style="background:#2f2f2d; color:#d7d2c8;">Wybierz produkt</span>
</div> </div>
${summaryNutrition ? `<p class="text-[10px] mt-2 tabular-nums" style="color:#9b978f;">${esc(macroLine(summaryNutrition))}</p>` : ''}
</div>`; </div>`;
return; return;
} }
@@ -272,41 +312,87 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
? (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 = product ? (product.packSize || pantryQtyStep(state.ingredientId)) : pantryQtyStep(state.ingredientId);
const stockNutrition = nutritionForQty(def, qty, product?.nutritionPer100g || def.nutritionPer100g); const packSize = product?.packSize || def.purchasePack?.amount || 0;
const packLabel = product?.packLabel || def.purchasePack?.label || '';
const draftQty = state.stockEditorOpen
? Math.max(0, Number(state.stockDraftQty ?? qty) || 0)
: qty;
const actionLabel = state.stockEditorOpen ? 'Anuluj' : 'Zmień';
wrap.innerHTML = ` wrap.innerHTML = `
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p> <p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p>
<div class="flex items-center justify-center gap-3 rounded-xl px-3 py-2" style="background:#393937;"> <div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
<button type="button" class="ingredient-card-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95" style="background:#2f2f2d; color:#d7d2c8;" data-pid="${esc(state.productId || '_generic')}" data-step="${step}" data-dir="-1" aria-label="Zmniejsz stan"> <div class="flex items-start justify-between gap-3">
<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>
</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'};">
${esc(actionLabel)}
</button>
</div>
${state.stockEditorOpen ? `
<div class="mt-3 pt-3 border-t" style="border-color:#444442;">
<div class="flex items-center gap-2">
<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="Zmniejsz szkic zapasu">
<i class="fas fa-minus text-xs"></i> <i class="fas fa-minus text-xs"></i>
</button> </button>
<div class="flex-1 text-center"> <label class="flex-1 rounded-xl px-3 py-2 flex items-center justify-center gap-2" style="background:#2f2f2d;">
<p class="text-[17px] font-bold tabular-nums" style="color:#6ee7b7;">${esc(formatQty(qty))} ${esc(unit)}</p> <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;">
<p class="text-[10px] mt-0.5" style="color:#9b978f;">Krok: ${esc(formatQty(step))} ${esc(unit)}</p> <span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(unit)}</span>
</div> </label>
<button type="button" class="ingredient-card-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95" style="background:#2f2f2d; color:#d7d2c8;" data-pid="${esc(state.productId || '_generic')}" data-step="${step}" data-dir="1" aria-label="Zwiększ stan"> <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>
${stockNutrition ? `<p class="text-[10px] mt-1.5 tabular-nums" style="color:#9b978f;">${esc(macroLine(stockNutrition))} na stanie</p>` : ''}`; <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-save inline-flex items-center rounded-full px-3 py-1.5 text-[11px] font-semibold" style="background:#ddd6ca; color:#2d2e2b;">Zapisz</button>
</div>
</div>` : ''}
</div>`;
wrap.querySelectorAll('.ingredient-card-stock-btn').forEach((btn) => { wrap.querySelector('.ingredient-card-stock-toggle')?.addEventListener('click', () => {
btn.addEventListener('click', () => { if (state.stockEditorOpen) {
const pid = btn.dataset.pid; state.stockEditorOpen = false;
const stepVal = Number(btn.dataset.step) || 1; state.stockDraftQty = null;
const dir = Number(btn.dataset.dir) || 1;
const pantryState = loadPantry();
if (pid === '_generic') {
const current = getPantryTotal(state.ingredientId, pantryState);
setPantryQty(state.ingredientId, Math.max(0, current + stepVal * dir));
} else { } else {
const items = getPantryProducts(state.ingredientId, pantryState); state.stockEditorOpen = true;
const current = items.find((i) => i.productId === pid)?.qty || 0; state.shopEditorOpen = false;
setPantryProductQty(state.ingredientId, pid, Math.max(0, current + stepVal * dir)); state.stockDraftQty = qty;
} }
render(); render();
state.onAfterChange?.();
}); });
wrap.querySelectorAll('.ingredient-card-stock-step').forEach((btn) => {
btn.addEventListener('click', () => {
const dir = Number(btn.dataset.dir) || 1;
const next = Math.max(0, Math.round(((Number(state.stockDraftQty ?? qty) || 0) + step * dir) * 100) / 100);
state.stockDraftQty = next;
render();
});
});
wrap.querySelector('.ingredient-card-stock-input')?.addEventListener('input', (event) => {
state.stockDraftQty = Math.max(0, Number(event.target.value) || 0);
});
wrap.querySelector('.ingredient-card-stock-clear')?.addEventListener('click', () => {
state.stockDraftQty = 0;
render();
});
wrap.querySelector('.ingredient-card-stock-save')?.addEventListener('click', () => {
const input = wrap.querySelector('.ingredient-card-stock-input');
const nextQty = Math.max(0, Math.round((Number(input?.value ?? state.stockDraftQty ?? qty) || 0) * 100) / 100);
if (state.productId) {
setPantryProductQty(state.ingredientId, state.productId, nextQty);
} else {
setPantryQty(state.ingredientId, nextQty);
}
state.stockEditorOpen = false;
state.stockDraftQty = null;
render();
state.onAfterChange?.();
}); });
} }
@@ -359,6 +445,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
const nextProductId = btn.dataset.productId || null; const nextProductId = btn.dataset.productId || null;
state.productId = nextProductId; state.productId = nextProductId;
state.selectedProductId = nextProductId; state.selectedProductId = nextProductId;
resetInlineEditors();
if (nextProductId) state.onProductChange?.(nextProductId); if (nextProductId) state.onProductChange?.(nextProductId);
render(); render();
state.onAfterChange?.(); state.onAfterChange?.();
@@ -377,28 +464,99 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
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 btnLabel = usesPacks const defaultAmount = usesPacks ? packSize : pantryQtyStep(state.ingredientId);
? `Dodaj na listę (${packLabel || `${formatQty(packSize)} ${unitLabel(def.pantryUnit)}`})` const shoppingItem = getCurrentShoppingItem(def);
: 'Dodaj na listę'; const hasShoppingItem = Boolean(shoppingItem);
const helperText = hasProducts && !product const shoppingAmount = shoppingItem?.amount || 0;
? 'Doda składnik bez wskazanej marki. Jeśli chcesz konkretny produkt, wybierz go wyżej.' const draftQty = state.shopEditorOpen
: product ? Math.max(0, Number(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) || 0)
? 'Pozycja trafi na listę zakupów z dokładnym produktem.' : shoppingAmount;
: `Szybki skrót do listy zakupów ${defaultSourceNote === 'Ze spiżarni' ? 'ze spiżarni' : 'z planera'}.`; const actionLabel = state.shopEditorOpen ? 'Anuluj' : 'Zmień';
wrap.innerHTML = ` wrap.innerHTML = `
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Lista zakupów</p> <p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Lista zakupów</p>
<p class="text-[10px] mb-1.5" style="color:#9b978f;">${esc(helperText)}</p> <div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
<button type="button" class="ingredient-card-add-list 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;"> <div class="flex items-start justify-between gap-3">
<i class="fas fa-cart-plus text-[11px]"></i>${esc(btnLabel)} <div class="min-w-0 flex-1">
</button>`; <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>
</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'};">
${esc(actionLabel)}
</button>
</div>
${state.shopEditorOpen ? `
<div class="mt-3 pt-3 border-t" style="border-color:#444442;">
<div class="flex items-center gap-2">
<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="Zmniejsz ilość na liście">
<i class="fas fa-minus text-xs"></i>
</button>
<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;">
<span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(unitLabel(def.pantryUnit))}</span>
</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">
<i class="fas fa-plus text-xs"></i>
</button>
</div>
<div class="flex items-center justify-between gap-3 mt-3">
${hasShoppingItem
? '<button type="button" class="ingredient-card-shop-remove text-[11px] font-semibold" style="color:#9b978f;">Usuń z listy</button>'
: '<span></span>'}
<button type="button" class="ingredient-card-shop-save inline-flex items-center rounded-full px-3 py-1.5 text-[11px] font-semibold" style="background:#ddd6ca; color:#2d2e2b;">Zapisz</button>
</div>
</div>` : ''}
</div>`;
wrap.querySelector('.ingredient-card-add-list')?.addEventListener('click', () => { wrap.querySelector('.ingredient-card-shop-toggle')?.addEventListener('click', () => {
const amount = usesPacks ? packSize : pantryQtyStep(state.ingredientId); if (state.shopEditorOpen) {
state.shopEditorOpen = false;
state.shopDraftQty = null;
} else {
state.shopEditorOpen = true;
state.stockEditorOpen = false;
state.shopDraftQty = shoppingAmount || defaultAmount;
}
render();
});
wrap.querySelectorAll('.ingredient-card-shop-step').forEach((btn) => {
btn.addEventListener('click', () => {
const dir = Number(btn.dataset.dir) || 1;
const next = Math.max(0, Math.round(((Number(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) || 0) + defaultAmount * dir) * 100) / 100);
state.shopDraftQty = next;
render();
});
});
wrap.querySelector('.ingredient-card-shop-input')?.addEventListener('input', (event) => {
state.shopDraftQty = Math.max(0, Number(event.target.value) || 0);
});
wrap.querySelector('.ingredient-card-shop-remove')?.addEventListener('click', () => {
if (!shoppingItem) return;
removeItemFromList(KITCHEN_LIST_ID, shoppingItem.id);
state.shopEditorOpen = false;
state.shopDraftQty = null;
render();
state.onAfterChange?.();
window.refreshShopping?.();
showAppToast(`Usunieto ${product?.name || def.name} z listy.`);
});
wrap.querySelector('.ingredient-card-shop-save')?.addEventListener('click', () => {
const input = wrap.querySelector('.ingredient-card-shop-input');
const nextAmount = Math.max(0, Math.round((Number(input?.value ?? state.shopDraftQty ?? defaultAmount) || 0) * 100) / 100);
let toastText = null;
if (shoppingItem) {
updateKitchenItemAmount(KITCHEN_LIST_ID, shoppingItem.id, nextAmount);
toastText = nextAmount > 0
? `Zaktualizowano ${product?.name || def.name}.`
: `Usunieto ${product?.name || def.name} z listy.`;
} else if (nextAmount > 0) {
const note = usesPacks ? (packLabel || `${formatQty(packSize)} ${unitLabel(def.pantryUnit)}`) : undefined; const note = usesPacks ? (packLabel || `${formatQty(packSize)} ${unitLabel(def.pantryUnit)}`) : undefined;
const line = { const line = {
ingredientId: state.ingredientId, ingredientId: state.ingredientId,
amount, amount: nextAmount,
unit: unitLabel(def.pantryUnit), unit: unitLabel(def.pantryUnit),
name: product?.name || def.name, name: product?.name || def.name,
category: def.category, category: def.category,
@@ -406,8 +564,14 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
}; };
if (state.productId) line.productId = state.productId; if (state.productId) line.productId = state.productId;
addOrMergeShoppingLines([line]); addOrMergeShoppingLines([line]);
showAppToast(`Dodano ${product?.name || def.name} na listę.`); toastText = `Dodano ${product?.name || def.name}.`;
}
state.shopEditorOpen = false;
state.shopDraftQty = null;
render();
state.onAfterChange?.();
window.refreshShopping?.(); window.refreshShopping?.();
if (toastText) showAppToast(toastText);
}); });
} }
@@ -444,6 +608,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
state.sourceNote = sourceNote; state.sourceNote = sourceNote;
state.onProductChange = onProductChange; state.onProductChange = onProductChange;
state.onAfterChange = onAfterChange; state.onAfterChange = onAfterChange;
resetInlineEditors();
render(); render();
clearTimeout(state.closeTimer); clearTimeout(state.closeTimer);
@@ -472,6 +637,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
state.onProductChange = null; state.onProductChange = null;
state.onAfterChange = null; state.onAfterChange = null;
state.sourceNote = defaultSourceNote; state.sourceNote = defaultSourceNote;
resetInlineEditors();
} }
function bind() { function bind() {
@@ -480,6 +646,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
el('close')?.addEventListener('click', close); el('close')?.addEventListener('click', close);
el('back')?.addEventListener('click', () => { el('back')?.addEventListener('click', () => {
state.productId = null; state.productId = null;
resetInlineEditors();
render(); render();
}); });
el('overlay')?.addEventListener('click', (event) => { el('overlay')?.addEventListener('click', (event) => {

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-99'; import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-106';
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;');

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-99'; import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-106';
/* ── helpers ── */ /* ── helpers ── */