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, '>') .replace(/"/g, '"'); } 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 ` `; } 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} */ const pantrySectionPins = {}; /** @type {Record>} */ 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 ``; }).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 `Kupujesz w: ${escapeHtml(hint)}`; } const bits = []; if (fullPacks > 0) bits.push(`${fullPacks}× opak.`); if (remainder > 0) bits.push(`+ ${remainder} ${u}`); return `${escapeHtml(bits.join(' '))} (${escapeHtml(hint)})`; } /** Mała ikona w prawym górnym rogu karty — rozwija panel w dół. */ function nutritionCornerToggle(ingredientId) { const panelId = `pantry-nut-${ingredientId}`; return ` `; } function nutritionListRow(label, valueHtml) { return `
  • ${escapeHtml(label)} ${valueHtml}
  • `; } 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 = ` `; return ` `; } 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 ? `` : ''; const headerInner = ` ${escapeHtml(title)} ${count} ${escapeHtml(hint)} ${chevron}`; const header = showToggle ? `` : `
    ${headerInner}
    `; return `
    ${header}
    ${bodyInner}
    `; } 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 ? `${escapeHtml(pack.label || `${pack.amount} ${unit}`)}` : ''; 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 `

    ${escapeHtml(def.name)}

    ${packPill}

    ${escapeHtml(categoryLabel(def.category))} · stan w ${unit}

    ${packStockCaption(def, qty)}
    ${hasNutrition ? nutritionCornerToggle(id) : ''}
    ${hasNutrition ? nutritionPanelHtml(def, id) : ''}
    ${escapeHtml(stepHint)}
    `; } 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 = '

    Brak wyników — zmień wyszukiwanie lub filtr kategorii.

    '; return; } const { have, catalogOnly } = splitHaveAndCatalog(ids, pantry); const haveBody = have.length ? `
    ${have.map((id) => pantryCardHtml(id, pantry, 'have')).join('')}
    ` : '

    Żaden z widocznych produktów nie ma jeszcze zapasu — ustaw ilość w katalogu poniżej.

    '; const catBody = catalogOnly.length ? `
    ${catalogOnly.map((id) => pantryCardHtml(id, pantry, 'catalog')).join('')}
    ` : '

    Wszystkie widoczne pozycje są na stanie.

    '; 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; }