Files
homelab/stacks/recipe/js/views/Pantry.js
2026-03-24 23:15:56 +01:00

589 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
INGREDIENTS,
CATEGORY_LABELS,
pantryQtyStep,
splitStockIntoPacks,
} from '../data/catalog.js';
import { addIngredientToKitchenList, categoryLabel, loadPantry, setPantryQty } from '../services/pantryShopping.js';
import { showAppToast } from '../ui/toast.js';
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function pantryUnitLabel(u) {
if (u === 'szt') return 'szt.';
return u;
}
function normalizeSearch(q) {
return String(q).trim().toLowerCase();
}
const PANTRY_SHOP_BOTTOM = '5.25rem';
const PANTRY_SHOP_OFF = `translateY(calc(100% + ${PANTRY_SHOP_BOTTOM}))`;
/** @type {string | null} */
let shopPickerIngredientId = null;
/** @type {number} */
let shopPickerStep = 1;
/** Czy licznik w arkuszu to liczba opakowań (vs. jednostki magazynowe). */
let shopPickerUsesPacks = false;
export function getPantryHTML() {
return `
<div id="pantry-view" class="hidden flex flex-col h-full absolute inset-0 overflow-hidden bg-gray-50 z-10 pb-24">
<div class="shrink-0 bg-white border-b border-gray-200 mt-3 px-4 pt-2 pb-3 space-y-3">
<div class="flex items-center w-full border border-gray-300 rounded-xl bg-white focus-within:border-gray-400 transition-colors">
<span class="pl-3 text-gray-400"><i class="fas fa-search text-sm"></i></span>
<input type="search" id="pantry-search" autocomplete="off" placeholder="Szukaj po nazwie…" class="flex-1 py-2.5 px-2 bg-transparent outline-none text-sm text-gray-800 placeholder-gray-400" />
</div>
<div id="pantry-category-filters" class="flex gap-1.5 overflow-x-auto no-scrollbar pb-0.5 -mx-1 px-1"></div>
</div>
<div id="pantry-scroll" class="flex-1 overflow-y-auto px-4 pt-3 pb-4 no-scrollbar">
<div id="pantry-results" class="space-y-2"></div>
</div>
<div id="pantry-shop-backdrop" class="absolute inset-0 z-[38] bg-black/40 hidden opacity-0 transition-opacity duration-200" aria-hidden="true"></div>
<div id="pantry-shop-sheet" class="absolute left-0 right-0 z-[40] bg-white rounded-t-3xl shadow-[0_-10px_40px_rgba(0,0,0,0.12)] px-4 pt-2 pb-5 flex flex-col gap-3 max-h-[55%] min-h-0" style="bottom: ${PANTRY_SHOP_BOTTOM}; transform: ${PANTRY_SHOP_OFF}; transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1)" role="dialog" aria-labelledby="pantry-shop-heading" aria-modal="true">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto shrink-0" aria-hidden="true"></div>
<div class="shrink-0">
<h2 id="pantry-shop-heading" class="text-lg font-bold text-gray-900 leading-tight"></h2>
<p id="pantry-shop-sub" class="text-xs text-gray-500 mt-1"></p>
</div>
<div class="flex items-center justify-center gap-4 py-2 shrink-0">
<button type="button" id="pantry-shop-minus" class="w-11 h-11 rounded-xl bg-gray-100 text-gray-800 hover:bg-gray-200 flex items-center justify-center transition-colors" aria-label="Mniej na liście"><i class="fas fa-minus text-sm"></i></button>
<input type="number" id="pantry-shop-qty" min="1" step="1" inputmode="numeric" class="w-24 text-center text-2xl font-bold tabular-nums border border-gray-200 rounded-xl py-2 outline-none focus:border-gray-400" value="1" />
<button type="button" id="pantry-shop-plus" class="w-11 h-11 rounded-xl bg-gray-100 text-gray-800 hover:bg-gray-200 flex items-center justify-center transition-colors" aria-label="Więcej na liście"><i class="fas fa-plus text-sm"></i></button>
</div>
<button type="button" id="pantry-shop-add" class="shrink-0 w-full py-3.5 rounded-xl bg-gray-900 text-white text-sm font-semibold hover:bg-black transition-colors">Dodaj na listę</button>
<button type="button" id="pantry-shop-cancel" class="shrink-0 w-full py-2 text-sm font-medium text-gray-500 hover:text-gray-800">Anuluj</button>
</div>
</div>
`;
}
let pantryFilterCategory = '';
/** Zwijanie sekcji (ignorowane przy aktywnym wyszukiwaniu — widać wszystkie trafienia). */
let pantryAccordionHaveOpen = true;
let pantryAccordionCatalogOpen = false;
/** Po zmianie ilości przez próg 0 ↔ zapas karta zostaje wizualnie w tej samej sekcji przez chwilę. */
const PANTRY_SECTION_PIN_MS = 1400;
/** @type {Record<string, { section: 'have'|'catalog', until: number }>} */
const pantrySectionPins = {};
/** @type {Record<string, ReturnType<typeof setTimeout>>} */
const pantryPinTimers = {};
/**
* @param {string} id
* @param {'have'|'catalog'} section
*/
function pinPantrySection(id, section) {
pantrySectionPins[id] = { section, until: Date.now() + PANTRY_SECTION_PIN_MS };
if (pantryPinTimers[id]) {
clearTimeout(pantryPinTimers[id]);
delete pantryPinTimers[id];
}
pantryPinTimers[id] = setTimeout(() => {
delete pantrySectionPins[id];
delete pantryPinTimers[id];
renderPantryResults();
}, PANTRY_SECTION_PIN_MS);
}
function allCategoryKeys() {
const s = new Set();
Object.values(INGREDIENTS).forEach((d) => s.add(d.category));
return [...s].sort((a, b) => categoryLabel(a).localeCompare(categoryLabel(b)));
}
function renderCategoryChips() {
const wrap = document.getElementById('pantry-category-filters');
if (!wrap) return;
const keys = allCategoryKeys();
const chips = [
{ id: '', label: 'Wszystkie' },
...keys.map((k) => ({ id: k, label: categoryLabel(k) })),
];
wrap.innerHTML = chips.map((c) => {
const active = c.id === pantryFilterCategory;
const cls = active
? 'shrink-0 px-3 py-1.5 rounded-full text-[11px] font-semibold bg-gray-900 text-white'
: 'shrink-0 px-3 py-1.5 rounded-full text-[11px] font-semibold bg-gray-100 text-gray-600 hover:bg-gray-200';
return `<button type="button" data-pantry-cat="${escapeHtml(c.id)}" class="pantry-cat-btn ${cls}">${escapeHtml(c.label)}</button>`;
}).join('');
wrap.querySelectorAll('.pantry-cat-btn').forEach((btn) => {
btn.addEventListener('click', () => {
pantryFilterCategory = btn.getAttribute('data-pantry-cat') || '';
renderCategoryChips();
renderPantryResults();
});
});
}
function filterIds(searchRaw) {
const q = normalizeSearch(searchRaw);
return Object.keys(INGREDIENTS)
.filter((id) => {
const d = INGREDIENTS[id];
if (pantryFilterCategory && d.category !== pantryFilterCategory) return false;
if (!q) return true;
const name = d.name.toLowerCase();
const cat = (CATEGORY_LABELS[d.category] || '').toLowerCase();
return name.includes(q) || cat.includes(q);
})
.sort((a, b) => INGREDIENTS[a].name.localeCompare(INGREDIENTS[b].name, 'pl'));
}
function packStockCaption(def, stockQty) {
const split = splitStockIntoPacks(def, stockQty);
if (!split || !def.purchasePack) return '';
const u = pantryUnitLabel(def.pantryUnit);
const hint = def.purchasePack.label || `${def.purchasePack.amount} ${u}`;
const { fullPacks, remainder } = split;
if (fullPacks <= 0 && remainder <= 0) {
return `<span class="text-[10px] text-gray-500 block mt-1">Kupujesz w: ${escapeHtml(hint)}</span>`;
}
const bits = [];
if (fullPacks > 0) bits.push(`${fullPacks}× opak.`);
if (remainder > 0) bits.push(`+ ${remainder} ${u}`);
return `<span class="text-[10px] text-gray-600 block mt-1">${escapeHtml(bits.join(' '))} <span class="text-gray-400">(${escapeHtml(hint)})</span></span>`;
}
/** Mała ikona w prawym górnym rogu karty — rozwija panel w dół. */
function nutritionCornerToggle(ingredientId) {
const panelId = `pantry-nut-${ingredientId}`;
return `
<button type="button"
class="pantry-nutrition-toggle shrink-0 -mr-0.5 -mt-0.5 flex h-8 w-8 items-center justify-center rounded-lg text-gray-400 hover:text-gray-700 hover:bg-white/90 active:bg-white border border-transparent hover:border-gray-200/70 transition-colors"
aria-expanded="false"
aria-controls="${escapeHtml(panelId)}"
title="Wartości odżywcze"
aria-label="Pokaż lub ukryj wartości odżywcze">
<i class="fas fa-chevron-down pantry-nutrition-chevron text-[11px] transition-transform duration-200" aria-hidden="true"></i>
</button>`;
}
function nutritionListRow(label, valueHtml) {
return `<li class="flex items-baseline justify-between gap-3 py-0.5 border-b border-gray-100/80 last:border-0">
<span class="text-gray-500 shrink-0">${escapeHtml(label)}</span>
<span class="text-right font-semibold tabular-nums text-gray-800">${valueHtml}</span>
</li>`;
}
function nutritionPanelHtml(def, ingredientId) {
const n = def.nutritionPer100g;
if (!n) return '';
const panelId = `pantry-nut-${ingredientId}`;
const refLabel = def.pantryUnit === 'ml' ? '100 ml produktu' : '100 g produktu';
const refList = `
<ul class="space-y-0 rounded-lg bg-white/70 px-2 py-1 ring-1 ring-gray-100/90">
${nutritionListRow('Energia', `${n.kcal} kcal`)}
${nutritionListRow('Białko', `${n.protein} g`)}
${nutritionListRow('Tłuszcz', `${n.fat} g`)}
${nutritionListRow('Węglowodany', `${n.carbs} g`)}
</ul>`;
return `
<div id="${escapeHtml(panelId)}"
class="pantry-nutrition-panel hidden text-[10px] leading-snug"
role="region"
aria-label="Wartości odżywcze">
<div class="mt-2 pt-2.5 border-t border-gray-200/70 space-y-1">
<p class="text-[9px] font-semibold uppercase tracking-wide text-gray-500 px-0.5">${escapeHtml(refLabel)}</p>
${refList}
</div>
</div>`;
}
function splitHaveAndCatalog(ids, pantry) {
const now = Date.now();
/** @type {string[]} */
const have = [];
/** @type {string[]} */
const catalogOnly = [];
for (const id of ids) {
const pin = pantrySectionPins[id];
if (pin && pin.until > now) {
if (pin.section === 'have') have.push(id);
else catalogOnly.push(id);
continue;
}
if (pin && pin.until <= now) {
delete pantrySectionPins[id];
if (pantryPinTimers[id]) {
clearTimeout(pantryPinTimers[id]);
delete pantryPinTimers[id];
}
}
const qty = Number(pantry[id]) || 0;
if (qty > 0) have.push(id);
else catalogOnly.push(id);
}
return { have, catalogOnly };
}
/**
* @param {'have' | 'catalog'} sectionKey
* @param {{ title: string, hint: string, count: number, tone: 'emerald' | 'slate', open: boolean, searching: boolean, bodyInner: string }} opts
*/
function pantryAccordionSection(sectionKey, opts) {
const { title, hint, count, tone, open, searching, bodyInner } = opts;
const showToggle = !searching && count > 0;
const isOpen = searching || open || count === 0;
const ring = tone === 'emerald' ? 'ring-emerald-100/90' : 'ring-gray-200/90';
const dot = tone === 'emerald' ? 'bg-emerald-500 shadow-[0_0_0_3px_rgba(16,185,129,0.2)]' : 'bg-slate-400 shadow-[0_0_0_3px_rgba(148,163,184,0.25)]';
const chevronRot = isOpen ? '' : '-rotate-90';
const rowCls =
'w-full flex items-center gap-3 px-3.5 py-3 text-left min-h-[3.25rem]' +
(showToggle ? ' hover:bg-gray-50/80 transition-colors pantry-acc-toggle cursor-pointer' : '');
const chevron =
showToggle
? `<span class="shrink-0 w-8 h-8 rounded-xl bg-gray-100 flex items-center justify-center text-gray-600" aria-hidden="true"><i class="fas fa-chevron-down text-[10px] transition-transform duration-200 pantry-acc-chevron ${chevronRot}"></i></span>`
: '';
const headerInner = `
<span class="${dot} w-2 h-2 rounded-full shrink-0" aria-hidden="true"></span>
<span class="flex-1 min-w-0">
<span class="flex items-baseline flex-wrap gap-x-2 gap-y-0.5">
<span class="text-[13px] font-bold text-gray-900 tracking-tight">${escapeHtml(title)}</span>
<span class="text-xs font-semibold tabular-nums text-gray-500">${count}</span>
</span>
<span class="block text-[11px] text-gray-500 mt-0.5">${escapeHtml(hint)}</span>
</span>
${chevron}`;
const header =
showToggle
? `<button type="button" class="${rowCls}" data-pantry-acc="${escapeHtml(sectionKey)}" aria-expanded="${isOpen}">${headerInner}</button>`
: `<div class="${rowCls}">${headerInner}</div>`;
return `
<section class="rounded-2xl bg-white border border-gray-200/90 shadow-sm ring-1 ${ring} overflow-hidden mb-3 last:mb-0" data-pantry-acc-wrap="${escapeHtml(sectionKey)}">
${header}
<div class="pantry-acc-panel px-2.5 pb-2.5 pt-0 ${isOpen ? '' : 'hidden'}" data-pantry-acc-panel="${escapeHtml(sectionKey)}">
${bodyInner}
</div>
</section>`;
}
function pantryCardHtml(id, pantry, variant) {
const def = INGREDIENTS[id];
const unit = pantryUnitLabel(def.pantryUnit);
const qty = Number(pantry[id]) || 0;
const val = qty > 0 ? String(Math.round(qty)) : '';
const step = pantryQtyStep(id);
const pack = def.purchasePack;
const packPill = pack
? `<span class="shrink-0 inline-flex items-center px-2 py-0.5 rounded-lg bg-violet-100 text-violet-800 text-[10px] font-bold">${escapeHtml(pack.label || `${pack.amount} ${unit}`)}</span>`
: '';
const stepHint = pack
? `+/: ${step} ${unit} (1 opak.)`
: `+/: ${step} ${unit}`;
const shell = variant === 'have'
? 'rounded-xl border border-emerald-200 bg-gradient-to-br from-emerald-50/80 to-white p-3 shadow-sm ring-1 ring-emerald-100/80'
: 'rounded-xl border border-dashed border-gray-200 bg-gray-50/90 p-3 shadow-sm';
const hasNutrition = Boolean(def.nutritionPer100g);
return `
<div class="${shell}" data-ingredient-id="${escapeHtml(id)}" data-pantry-variant="${variant}">
<div class="flex items-start justify-between gap-1 mb-1">
<div class="min-w-0 flex-1 pr-1">
<div class="flex items-start gap-2 flex-wrap">
<p class="text-sm font-semibold text-gray-900">${escapeHtml(def.name)}</p>
${packPill}
</div>
<p class="text-[10px] text-gray-500 mt-0.5">${escapeHtml(categoryLabel(def.category))} · stan w ${unit}</p>
${packStockCaption(def, qty)}
</div>
${hasNutrition ? nutritionCornerToggle(id) : ''}
</div>
${hasNutrition ? nutritionPanelHtml(def, id) : ''}
<div class="flex flex-wrap items-center gap-2 mt-2.5">
<div class="flex items-center gap-1 ${variant === 'have' ? 'bg-emerald-100/60' : 'bg-gray-100'} rounded-lg p-0.5">
<button type="button" class="pantry-qty-minus w-8 h-8 rounded-md bg-white shadow-sm text-gray-700 hover:text-gray-900 flex items-center justify-center" aria-label="Mniej"><i class="fas fa-minus text-[10px]"></i></button>
<input type="number" min="0" step="1" inputmode="decimal" data-pantry-qty data-pantry-step="${step}"
class="w-[4.25rem] text-center text-sm font-semibold tabular-nums bg-transparent border-0 outline-none py-1"
value="${val}" placeholder="0" title="${escapeHtml(stepHint)}" />
<button type="button" class="pantry-qty-plus w-8 h-8 rounded-md bg-white shadow-sm text-gray-700 hover:text-gray-900 flex items-center justify-center" aria-label="Więcej"><i class="fas fa-plus text-[10px]"></i></button>
</div>
<span class="text-[10px] text-gray-400">${escapeHtml(stepHint)}</span>
<button type="button" class="pantry-add-shop ml-auto flex items-center gap-1.5 px-3 py-2 rounded-lg bg-gray-900 text-white text-xs font-semibold hover:bg-black transition-colors">
<i class="fas fa-cart-plus text-[10px]"></i>
Na listę…
</button>
</div>
</div>`;
}
function renderPantryResults() {
const root = document.getElementById('pantry-results');
if (!root) return;
const searchEl = document.getElementById('pantry-search');
const q = searchEl?.value || '';
const searching = normalizeSearch(q) !== '';
const pantry = loadPantry();
const ids = filterIds(q);
if (ids.length === 0) {
root.innerHTML = '<p class="text-sm text-gray-500 text-center py-10">Brak wyników — zmień wyszukiwanie lub filtr kategorii.</p>';
return;
}
const { have, catalogOnly } = splitHaveAndCatalog(ids, pantry);
const haveBody = have.length
? `<div class="space-y-2">${have.map((id) => pantryCardHtml(id, pantry, 'have')).join('')}</div>`
: '<p class="text-xs text-gray-500 text-center py-6 px-2">Żaden z widocznych produktów nie ma jeszcze zapasu — ustaw ilość w katalogu poniżej.</p>';
const catBody = catalogOnly.length
? `<div class="space-y-2">${catalogOnly.map((id) => pantryCardHtml(id, pantry, 'catalog')).join('')}</div>`
: '<p class="text-xs text-gray-500 text-center py-6 px-2">Wszystkie widoczne pozycje są na stanie.</p>';
const haveHint =
have.length === 0
? 'Brak zapasu w tym widoku'
: have.length === 1
? '1 produkt z zapasem'
: `${have.length} produktów z zapasem`;
const catHint =
catalogOnly.length === 0
? 'Nic do uzupełnienia w tym widoku'
: catalogOnly.length === 1
? '1 pozycja bez zapasu'
: `${catalogOnly.length} pozycji bez zapasu`;
root.innerHTML =
pantryAccordionSection('have', {
title: 'Na stanie',
hint: haveHint,
count: have.length,
tone: 'emerald',
open: pantryAccordionHaveOpen,
searching,
bodyInner: haveBody,
}) +
pantryAccordionSection('catalog', {
title: 'Katalog — bez zapasu',
hint: catHint,
count: catalogOnly.length,
tone: 'slate',
open: pantryAccordionCatalogOpen,
searching,
bodyInner: catBody,
});
root.querySelectorAll('.pantry-acc-toggle').forEach((btn) => {
btn.addEventListener('click', () => {
const key = btn.getAttribute('data-pantry-acc');
if (key === 'have') pantryAccordionHaveOpen = !pantryAccordionHaveOpen;
else if (key === 'catalog') pantryAccordionCatalogOpen = !pantryAccordionCatalogOpen;
renderPantryResults();
});
});
root.querySelectorAll('[data-ingredient-id]').forEach((card) => {
const id = card.getAttribute('data-ingredient-id');
if (!id) return;
const input = card.querySelector('[data-pantry-qty]');
const step = parseFloat(String(input?.getAttribute('data-pantry-step'))) || pantryQtyStep(id);
const applyQty = (n) => {
const v = Math.max(0, Math.round(Number(n) * 1000) / 1000 || 0);
setPantryQty(id, v);
if (input) {
input.value = v > 0 ? String(v) : '';
}
const prevVariant = card.getAttribute('data-pantry-variant');
const nowHave = v > 0;
const expectVariant = nowHave ? 'have' : 'catalog';
if (prevVariant === 'have' || prevVariant === 'catalog') {
if (prevVariant !== expectVariant) {
const existing = pantrySectionPins[id];
const t = Date.now();
const extending = Boolean(
existing && existing.until > t && existing.section === prevVariant,
);
pinPantrySection(id, prevVariant);
if (!extending) {
renderPantryResults();
}
}
}
};
card.querySelector('.pantry-qty-minus')?.addEventListener('click', () => {
const cur = parseFloat(String(input?.value).replace(',', '.')) || 0;
applyQty(Math.max(0, cur - step));
});
card.querySelector('.pantry-qty-plus')?.addEventListener('click', () => {
const cur = parseFloat(String(input?.value).replace(',', '.')) || 0;
applyQty(cur + step);
});
input?.addEventListener('change', () => {
const raw = String(input.value).replace(',', '.').trim();
const v = raw === '' ? 0 : parseFloat(raw);
applyQty(Number.isFinite(v) ? v : 0);
});
card.querySelector('.pantry-add-shop')?.addEventListener('click', () => {
openPantryShopPicker(id);
});
card.querySelector('.pantry-nutrition-toggle')?.addEventListener('click', (ev) => {
ev.preventDefault();
const btn = /** @type {HTMLButtonElement} */ (ev.currentTarget);
const panel = card.querySelector('.pantry-nutrition-panel');
const chevron = card.querySelector('.pantry-nutrition-chevron');
if (!panel) return;
const willOpen = panel.classList.contains('hidden');
panel.classList.toggle('hidden', !willOpen);
btn.setAttribute('aria-expanded', String(willOpen));
chevron?.classList.toggle('rotate-180', willOpen);
});
});
}
function readShopPickerQty() {
const el = document.getElementById('pantry-shop-qty');
const raw = parseFloat(String(el?.value).replace(',', '.')) || 0;
return Math.max(1, Math.round(raw));
}
function setShopPickerQtyDisplay(v) {
const el = document.getElementById('pantry-shop-qty');
if (el) el.value = String(Math.max(1, Math.round(Number(v))));
}
function openPantryShopPicker(ingredientId) {
const def = INGREDIENTS[ingredientId];
if (!def) return;
shopPickerIngredientId = ingredientId;
const unit = pantryUnitLabel(def.pantryUnit);
const pack = def.purchasePack;
shopPickerUsesPacks = Boolean(pack && pack.amount > 0);
const heading = document.getElementById('pantry-shop-heading');
const sub = document.getElementById('pantry-shop-sub');
if (shopPickerUsesPacks) {
shopPickerStep = 1;
if (heading) heading.textContent = `Ile opakowań: ${def.name}?`;
if (sub) {
const lab = pack.label || `${pack.amount} ${unit}`;
sub.textContent = `Jedno = ${lab}. Na listę trafi suma w ${unit} (${lab}).`;
}
setShopPickerQtyDisplay(1);
} else {
shopPickerStep = pantryQtyStep(ingredientId);
if (heading) heading.textContent = `Ile dodać: ${def.name}?`;
if (sub) {
sub.textContent = `Jednostka na liście: ${unit}. Przyciski +/: ${shopPickerStep} ${unit}.`;
}
setShopPickerQtyDisplay(shopPickerStep);
}
const backdrop = document.getElementById('pantry-shop-backdrop');
const sheet = document.getElementById('pantry-shop-sheet');
if (!backdrop || !sheet) return;
sheet.classList.remove('hidden');
backdrop.classList.remove('hidden');
requestAnimationFrame(() => {
backdrop.classList.remove('opacity-0');
sheet.style.transform = 'translateY(0)';
});
}
function closePantryShopPicker() {
shopPickerIngredientId = null;
shopPickerUsesPacks = false;
const backdrop = document.getElementById('pantry-shop-backdrop');
const sheet = document.getElementById('pantry-shop-sheet');
if (sheet) {
sheet.style.transform = PANTRY_SHOP_OFF;
}
if (backdrop) {
backdrop.classList.add('opacity-0');
}
setTimeout(() => {
backdrop?.classList.add('hidden');
sheet?.classList.add('hidden');
}, 300);
}
function bindPantryShopSheet() {
document.getElementById('pantry-shop-backdrop')?.addEventListener('click', closePantryShopPicker);
document.getElementById('pantry-shop-cancel')?.addEventListener('click', closePantryShopPicker);
document.getElementById('pantry-shop-minus')?.addEventListener('click', () => {
const cur = readShopPickerQty();
const dec = shopPickerUsesPacks ? 1 : shopPickerStep;
setShopPickerQtyDisplay(Math.max(1, cur - dec));
});
document.getElementById('pantry-shop-plus')?.addEventListener('click', () => {
const cur = readShopPickerQty();
const inc = shopPickerUsesPacks ? 1 : shopPickerStep;
setShopPickerQtyDisplay(cur + inc);
});
document.getElementById('pantry-shop-qty')?.addEventListener('change', () => {
setShopPickerQtyDisplay(readShopPickerQty());
});
document.getElementById('pantry-shop-add')?.addEventListener('click', () => {
if (!shopPickerIngredientId) return;
const def = INGREDIENTS[shopPickerIngredientId];
if (!def) return;
const count = readShopPickerQty();
const unit = pantryUnitLabel(def.pantryUnit);
if (shopPickerUsesPacks && def.purchasePack) {
const packAmt = def.purchasePack.amount;
const total = count * packAmt;
const note = `${count}× ${def.purchasePack.label || `${packAmt} ${unit}`}`;
addIngredientToKitchenList(shopPickerIngredientId, total, note);
showAppToast(`Dodano ${count} op. (${total} ${unit}) na listę kuchni.`);
} else {
addIngredientToKitchenList(shopPickerIngredientId, count);
showAppToast(`Dodano ${count} ${unit} na listę kuchni.`);
}
closePantryShopPicker();
window.refreshShopping?.();
});
}
export function refreshPantry() {
renderCategoryChips();
renderPantryResults();
}
export function setupPantry() {
renderCategoryChips();
renderPantryResults();
bindPantryShopSheet();
document.getElementById('pantry-search')?.addEventListener('input', () => {
renderPantryResults();
});
window.refreshPantry = refreshPantry;
}