import { INGREDIENTS } from '../data/catalog.js?v=9'; import { loadPantry, computeShortfalls, categoryLabel, addAmountToPantry, subtractFromPantry, getSelectedDays, setSelectedDays, addSessionLogEntry, removeSessionLogEntry, clearSessionLog, loadShoppingSession, } from '../services/pantryShopping.js?v=2'; import { aggregateSelectedDaysIngredientNeed } from '../services/planIngredients.js?v=2'; import { loadPlans, dateKey } from '../services/planStore.js?v=2'; import { addDays, startOfDay, startOfMonth } from '../services/dateUtils.js'; import { createSwipePopoverCalendarHTML, initSwipePopoverCalendar } from '../ui/swipePopoverCalendar.js'; import { createCalendarPopoverController, createCalendarPopoverHTML } from '../ui/calendarPopover.js'; import { showAppToast } from '../ui/toast.js'; /* ── helpers ── */ function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function formatQty(n) { const rounded = Math.round((Number(n) || 0) * 10) / 10; return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1).replace(/\.0$/, ''); } function unitLabel(u) { return u === 'szt' ? 'szt.' : String(u); } function stepForUnit(unit) { return (unit === 'szt.' || unit === 'szt') ? 1 : 10; } const CATEGORY_ICONS = { pieczywo: 'fa-bread-slice', nabial: 'fa-cheese', mieso_ryby: 'fa-drumstick-bite', warzywa: 'fa-carrot', owoce: 'fa-apple-whole', suche: 'fa-wheat-awn', przyprawy: 'fa-leaf', inne: 'fa-jar', }; const CATEGORY_ORDER = ['pieczywo', 'nabial', 'mieso_ryby', 'warzywa', 'owoce', 'suche', 'przyprawy', 'inne']; const DAY_ABBR = ['Nd', 'Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'Sb']; const DAY_NAMES_SHORT = ['nd.', 'pon.', 'wt.', 'śr.', 'czw.', 'pt.', 'sob.']; const WEEKDAY_SHORT = ['pn', 'wt', 'śr', 'cz', 'pt', 'sb', 'nd']; const MONTHS_LONG = ['Styczeń','Luty','Marzec','Kwiecień','Maj','Czerwiec','Lipiec','Sierpień','Wrzesień','Październik','Listopad','Grudzień']; const MONTHS_SHORT = ['sty','lut','mar','kwi','maj','cze','lip','sie','wrz','paź','lis','gru']; const CALENDAR_DIM_TEXT = 'rgb(var(--text-faint-rgb))'; const CALENDAR_DIM_OPACITY = '0.58'; /* ── module state ── */ let boughtPopupOpen = false; let expandedIngredientId = null; let expandedAmount = 0; let calendarOpen = false; let calendarMonth = startOfMonth(new Date()); let shoppingCalendar = null; let shoppingCalendarPopover = null; /* ── day helpers ── */ function todayKey() { return dateKey(new Date()); } function getDefaultSelectedDays() { const today = startOfDay(new Date()); return Array.from({ length: 7 }, (_, i) => dateKey(addDays(today, i))); } function formatRangePill(selectedDays) { if (selectedDays.length === 0) return 'Wybierz dni'; const sorted = [...selectedDays].sort(); const fmt = (dk) => { const d = new Date(dk + 'T00:00:00'); return `${DAY_NAMES_SHORT[d.getDay()]} ${d.getDate()} ${MONTHS_SHORT[d.getMonth()]}`; }; if (sorted.length === 1) return fmt(sorted[0]); return `${fmt(sorted[0])} – ${fmt(sorted[sorted.length - 1])}`; } /* ── computed data ── */ function computeActiveItems() { const selectedDays = getSelectedDays(); if (selectedDays.length === 0) return []; const plans = loadPlans(); const needLines = aggregateSelectedDaysIngredientNeed(plans, selectedDays); return computeShortfalls(needLines, loadPantry()); } function getSessionLog() { return loadShoppingSession().sessionLog; } function groupByCategory(items) { const groups = new Map(); for (const item of items) { const cat = item.category || 'inne'; if (!groups.has(cat)) groups.set(cat, []); groups.get(cat).push(item); } return CATEGORY_ORDER .filter((cat) => groups.has(cat)) .map((cat) => ({ cat, items: groups.get(cat) })); } /* ══════════════════════ HTML SHELL ══════════════════════ */ export function getShoppingListHTML() { return ` `; } /* ══════════════════════ CALENDAR ══════════════════════ */ function initShoppingCalendar() { shoppingCalendar = initSwipePopoverCalendar({ idPrefix: 'sl-cal', selectionMode: 'range', monthsLong: MONTHS_LONG, getMonthAnchor: () => calendarMonth, setMonthAnchor: (nextMonth) => { calendarMonth = startOfMonth(nextMonth); }, getSelectionKeys: () => getSelectedDays(), onSelectionCommit: (rangeKeys) => { setSelectedDays(rangeKeys); expandedIngredientId = null; updatePillLabel(); renderAll(); }, resolveDayState: (day, { inCurrentMonth, isSelected }) => ({ disabled: false, dimmed: !inCurrentMonth, showDot: dateKey(day) === todayKey() && !isSelected, }), theme: { selectedBorder: 'rgba(var(--text-emphasis-rgb),0.34)', selectedText: 'rgb(var(--text-emphasis-rgb))', selectedDot: 'rgb(var(--text-emphasis-rgb))', selectedShadow: '0 0 0 1px rgba(var(--text-emphasis-rgb),0.10)', bg: 'rgb(var(--app-bg-rgb))', border: 'transparent', text: 'rgb(var(--text-body-soft-rgb))', dimmedBg: 'transparent', dimText: CALENDAR_DIM_TEXT, dimOpacity: Number(CALENDAR_DIM_OPACITY), dot: 'rgb(var(--text-faint-rgb))', }, }); shoppingCalendarPopover = createCalendarPopoverController({ popupId: 'sl-calendar-popup', viewportId: 'sl-cal-viewport', triggerId: 'sl-range-pill', chevronId: 'sl-range-chevron', getCalendar: () => shoppingCalendar, hideViewportDuringLayout: true, }); } function updatePillLabel() { const el = document.getElementById('sl-range-label'); if (el) el.textContent = formatRangePill(getSelectedDays()); } function openCalendar() { if (boughtPopupOpen) closeBoughtPopup(); calendarOpen = true; calendarMonth = startOfMonth(new Date()); shoppingCalendarPopover?.open(); } function closeCalendar() { calendarOpen = false; shoppingCalendarPopover?.close({ clearPendingRange: true }); } function toggleCalendar() { calendarOpen ? closeCalendar() : openCalendar(); } function openBoughtPopup() { if (calendarOpen) closeCalendar(); boughtPopupOpen = true; const popup = document.getElementById('sl-bought-popup'); const btn = document.getElementById('sl-bought-btn'); if (popup) { popup.style.pointerEvents = 'auto'; popup.style.opacity = '1'; popup.style.transform = 'translateY(0) scale(1)'; } if (btn) { btn.style.background = 'rgb(var(--sunken-rgb))'; btn.style.borderColor = 'rgb(var(--border-input-rgb))'; } } function closeBoughtPopup() { boughtPopupOpen = false; const popup = document.getElementById('sl-bought-popup'); const btn = document.getElementById('sl-bought-btn'); if (popup) { popup.style.pointerEvents = 'none'; popup.style.opacity = '0'; popup.style.transform = 'translateY(-6px) scale(0.98)'; } if (btn) { btn.style.background = 'rgb(var(--card-rgb))'; btn.style.borderColor = 'rgb(var(--border-card-rgb))'; } } function toggleBoughtPopup() { boughtPopupOpen ? closeBoughtPopup() : openBoughtPopup(); } /* ══════════════════════ ITEM ROWS ══════════════════════ */ function activeItemHtml(item) { const def = INGREDIENTS[item.ingredientId]; const icon = CATEGORY_ICONS[def?.category] || 'fa-jar'; const image = def?.image; const mediaFit = image?.endsWith('.svg') ? 'object-contain' : 'object-cover'; const mediaHtml = image ? `` : `
`; const isExpanded = expandedIngredientId === item.ingredientId; const step = stepForUnit(item.unit); const stepAmt = isExpanded ? expandedAmount : Math.max(step, Math.round(item.shortfall / step) * step); return `
${mediaHtml}
${esc(item.name)}
${esc(formatQty(item.shortfall))} ${esc(unitLabel(item.unit))}
${esc(formatQty(stepAmt))} ${esc(unitLabel(item.unit))}
`; } function boughtItemHtml(entry) { const def = INGREDIENTS[entry.ingredientId]; const icon = CATEGORY_ICONS[def?.category] || 'fa-jar'; const image = def?.image; const mediaFit = image?.endsWith('.svg') ? 'object-contain' : 'object-cover'; const mediaHtml = image ? `` : `
`; return `
${mediaHtml}
${esc(entry.name)}
${esc(formatQty(entry.addedAmount))} ${esc(unitLabel(entry.unit))}
`; } /* ══════════════════════ SWIPE ══════════════════════ */ function attachSwipe(container, opts) { const inner = container.querySelector('.sl-swipe-inner'); if (!inner) return; const bgBuy = container.querySelector('.sl-swipe-bg-buy'); const bgUndo = container.querySelector('.sl-swipe-bg-undo'); const showBg = (el) => { if (el) el.style.opacity = '1'; }; const hideBgs = () => { if (bgBuy) bgBuy.style.opacity = '0'; if (bgUndo) bgUndo.style.opacity = '0'; }; let startX = 0, startY = 0, dx = 0, tracking = false, decided = false, goingH = false; container.addEventListener('pointerdown', (e) => { startX = e.clientX; startY = e.clientY; dx = 0; tracking = true; decided = false; goingH = false; inner.style.transition = 'none'; container.setPointerCapture(e.pointerId); }); container.addEventListener('pointermove', (e) => { if (!tracking) return; const ddx = e.clientX - startX; const ddy = e.clientY - startY; if (!decided) { if (Math.abs(ddx) < 6 && Math.abs(ddy) < 6) return; decided = true; goingH = Math.abs(ddx) > Math.abs(ddy); } if (!goingH) { tracking = false; inner.style.transform = ''; hideBgs(); return; } dx = ddx; if (dx > 0 && opts.onRight) { inner.style.transform = `translateX(${Math.min(dx, 90)}px)`; showBg(bgBuy); } else if (dx < 0 && opts.onLeft) { inner.style.transform = `translateX(${Math.max(dx, -90)}px)`; showBg(bgUndo); } }); const finish = () => { if (!tracking) return; tracking = false; inner.style.transition = 'transform 0.2s ease'; const thr = 65; if (dx > thr && opts.onRight) { inner.style.transform = 'translateX(120%)'; setTimeout(opts.onRight, 180); } else if (dx < -thr && opts.onLeft) { inner.style.transform = 'translateX(-120%)'; setTimeout(opts.onLeft, 180); } else { inner.style.transform = ''; hideBgs(); } dx = 0; }; container.addEventListener('pointerup', finish); container.addEventListener('pointercancel', () => { tracking = false; inner.style.transition = 'transform 0.2s ease'; inner.style.transform = ''; hideBgs(); dx = 0; }); } /* ══════════════════════ EXPAND / STEPPER ══════════════════════ */ function toggleExpand(ingredientId, unit, shortfall) { if (expandedIngredientId === ingredientId) { expandedIngredientId = null; } else { expandedIngredientId = ingredientId; const step = stepForUnit(unit); expandedAmount = Math.max(step, Math.round(shortfall / step) * step); } renderBoard(); } function updateExpandedAmountDisplay() { const el = document.querySelector(`[data-step-for="${expandedIngredientId}"] .sl-exp-amount`); if (el) el.textContent = formatQty(expandedAmount); } /* ══════════════════════ BUY / UNDO ══════════════════════ */ function buyItem(ingredientId, unit, amount) { const def = INGREDIENTS[ingredientId]; if (!def) return; addAmountToPantry(ingredientId, undefined, amount); addSessionLogEntry({ ingredientId, name: def.name, addedAmount: amount, unit, category: def.category || 'inne' }); expandedIngredientId = null; renderAll(); if (typeof window.refreshPantry === 'function') window.refreshPantry(); } function undoBoughtEntry(entryId) { const log = getSessionLog(); const entry = log.find((e) => e.id === entryId); if (!entry) return; subtractFromPantry(entry.ingredientId, entry.productId, entry.addedAmount); removeSessionLogEntry(entryId); renderAll(); if (typeof window.refreshPantry === 'function') window.refreshPantry(); showAppToast('Cofnięto zakup'); } /* ══════════════════════ RENDERING ══════════════════════ */ function renderBoughtBadge(count) { const badge = document.getElementById('sl-bought-badge'); if (!badge) return; if (count > 0) { badge.textContent = String(count); badge.style.display = 'flex'; } else { badge.style.display = 'none'; } } function renderBoard() { const root = document.getElementById('sl-board'); if (!root) return; const selectedDays = getSelectedDays(); if (selectedDays.length === 0) { root.innerHTML = `

Wybierz dni

Otwórz kalendarz i zaznacz dni, na które chcesz zrobić zakupy.

`; return; } const activeItems = computeActiveItems(); if (activeItems.length === 0) { root.innerHTML = `

Wszystko masz

Spiżarnia pokrywa zapotrzebowanie na wybrane dni.

`; return; } const groups = groupByCategory(activeItems); root.innerHTML = groups.map(({ cat, items: catItems }) => { const icon = CATEGORY_ICONS[cat] || 'fa-jar'; return `

${esc(categoryLabel(cat))}

${catItems.length}
${catItems.map((item) => activeItemHtml(item)).join('')}
`; }).join(''); bindActiveRowEvents(root, activeItems); } function renderBought() { const sessionLog = getSessionLog(); const countEl = document.getElementById('sl-bought-popup-count'); const list = document.getElementById('sl-bought-list'); if (!list || !countEl) return; countEl.textContent = String(sessionLog.length); if (sessionLog.length === 0) { list.innerHTML = `
Brak kupionych
`; return; } list.innerHTML = [...sessionLog].reverse().map((e) => boughtItemHtml(e)).join(''); list.querySelectorAll('.sl-swipe-wrap').forEach((wrap) => { const entryId = wrap.querySelector('.sl-swipe-inner')?.dataset.entryId; if (!entryId) return; attachSwipe(wrap, { onLeft: () => undoBoughtEntry(entryId) }); }); } function renderAll() { const sessionLog = getSessionLog(); renderBoughtBadge(sessionLog.length); renderBoard(); renderBought(); } /* ══════════════════════ ACTIVE ROW EVENTS ══════════════════════ */ function bindActiveRowEvents(root, activeItems) { root.querySelectorAll('.sl-swipe-wrap').forEach((wrap) => { const inner = wrap.querySelector('.sl-swipe-inner'); const ingredientId = inner?.dataset.id; const unit = inner?.dataset.unit; const shortfall = parseFloat(inner?.dataset.shortfall || '0'); if (!ingredientId) return; const item = activeItems.find((i) => i.ingredientId === ingredientId); attachSwipe(wrap, { onRight: item ? () => buyItem(item.ingredientId, item.unit, item.shortfall) : undefined }); inner.querySelector('.sl-item-main')?.addEventListener('click', () => toggleExpand(ingredientId, unit, shortfall)); inner.querySelector('.sl-step-minus')?.addEventListener('click', (e) => { e.stopPropagation(); const step = stepForUnit(unit); expandedAmount = Math.max(step, expandedAmount - step); updateExpandedAmountDisplay(); }); inner.querySelector('.sl-step-plus')?.addEventListener('click', (e) => { e.stopPropagation(); expandedAmount += stepForUnit(unit); updateExpandedAmountDisplay(); }); inner.querySelector('.sl-step-confirm')?.addEventListener('click', (e) => { e.stopPropagation(); if (expandedAmount > 0) buyItem(ingredientId, unit, expandedAmount); }); }); } /* ══════════════════════ PUBLIC API ══════════════════════ */ export function refreshShoppingList() { updatePillLabel(); renderAll(); if (calendarOpen) shoppingCalendar?.render(); } export function setupShoppingList() { if (getSelectedDays().length === 0) setSelectedDays(getDefaultSelectedDays()); updatePillLabel(); renderAll(); initShoppingCalendar(); document.getElementById('sl-range-pill')?.addEventListener('click', (e) => { e.stopPropagation(); toggleCalendar(); }); document.getElementById('sl-bought-btn')?.addEventListener('click', (e) => { e.stopPropagation(); toggleBoughtPopup(); }); document.addEventListener('click', (e) => { if (calendarOpen) { const popup = document.getElementById('sl-calendar-popup'); const pill = document.getElementById('sl-range-pill'); if (popup && !popup.contains(e.target) && pill && !pill.contains(e.target)) { closeCalendar(); } } if (boughtPopupOpen) { const popup = document.getElementById('sl-bought-popup'); const btn = document.getElementById('sl-bought-btn'); if (popup && !popup.contains(e.target) && btn && !btn.contains(e.target)) { closeBoughtPopup(); } } }); document.getElementById('sl-clear-session')?.addEventListener('click', (e) => { e.stopPropagation(); clearSessionLog(); renderAll(); }); window.refreshShoppingList = refreshShoppingList; }