import { INGREDIENTS, CATEGORY_LABELS, } from '../data/catalog.js?v=8'; import { loadPantry, getPantryTotal } from '../services/pantryShopping.js?v=2'; import { loadPlans } from '../services/planStore.js?v=2'; import { addDays, addMonths, sameDay, sameMonth, startOfDay, startOfMonth } from '../services/dateUtils.js'; import { aggregateRangeIngredientNeed, dayHasAnyMeal } from '../services/planIngredients.js?v=4'; import { bindCalendarDayClicks, createCalendarTopbarHTML, createCalendarWeekdayHeaderHTML, renderCalendarGrid, syncCalendarTodayButton, } from '../ui/mealCalendar.js?v=11'; import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260417-113'; /* ── helpers ── */ function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function unitLabel(u) { return u === 'szt' ? 'szt.' : u; } function normalizeSearch(q) { return String(q).trim().toLowerCase(); } function formatQty(n) { const rounded = Math.round((Number(n) || 0) * 10) / 10; return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1).replace(/\.0$/, ''); } function toggleStringFilter(list, value) { return list.includes(value) ? list.filter((item) => item !== value) : [...list, value]; } function hasActivePantryFilters() { return pantryFilters.categories.length > 0 || pantryFilters.sections.length > 0; } function getActivePantryFilterCount() { return pantryFilters.categories.length + pantryFilters.sections.length; } function filterChipStyle(active) { const background = active ? '#393937' : '#2d2e2b'; const color = active ? '#f2efe8' : '#d7d2c8'; const borderRule = active ? 'border:1px solid #787876;' : 'border:none;'; const shadow = active ? 'box-shadow:inset 0 1px 0 rgba(255,255,255,0.04), 0 0 0 1px rgba(0,0,0,0.08);' : ''; return `background:${background}; ${borderRule} color:${color}; ${shadow}`; } 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 PANTRY_SECTION_FILTERS = [ { id: 'shortfalls', label: 'Potrzebne' }, { id: 'sufficient', label: 'W spiżarni' }, { id: 'notPlanned', label: 'Poza planem' }, ]; const DAY_NAMES_SHORT = ['nd.', 'pon.', 'wt.', 'śr.', 'czw.', 'pt.', 'sob.']; const MONTHS_SHORT = ['sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', 'sie', 'wrz', 'paź', 'lis', 'gru']; const SEARCH_SHELL_SHADOW = '0 5px 10px rgba(0,0,0,0.16), 0 14px 22px rgba(0,0,0,0.24), 0 22px 34px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.04)'; const DEFAULT_HORIZON_DAYS = 7; const PANTRY_CALENDAR_DAY_ATTR = 'data-pantry-calendar-day'; const SHORTFALL_ACCENT = '#CB4A48'; const PANTRY_CALENDAR_THEME = { bg: '#272622', border: '#34312c', text: '#d7d2c8', dimText: '#5a5752', dimOpacity: 0.38, dimmedBg: 'transparent', dimmedBorder: 'transparent', dot: '#7d7a74', selectedBg: '#393937', selectedBorder: '#787876', selectedText: '#f2efe8', selectedDot: '#f2efe8', }; /* ── state ── */ let ingredientCard = null; let horizonEndDate = addDays(startOfDay(new Date()), DEFAULT_HORIZON_DAYS - 1); let isSearchExpanded = false; let isCalendarOpen = false; let isFilterOpen = false; let calendarMonthAnchor = startOfMonth(horizonEndDate); let pantryGlobalListenersBound = false; let pantryFilters = { categories: [], sections: [], }; /* ── date formatting ── */ function getToday() { return startOfDay(new Date()); } function ensureValidHorizonDate() { const today = getToday(); if (!(horizonEndDate instanceof Date) || Number.isNaN(horizonEndDate.getTime()) || horizonEndDate.getTime() < today.getTime()) { horizonEndDate = today; } if (!(calendarMonthAnchor instanceof Date) || Number.isNaN(calendarMonthAnchor.getTime())) { calendarMonthAnchor = startOfMonth(horizonEndDate); } } function getHorizonDays() { ensureValidHorizonDate(); const diffMs = startOfDay(horizonEndDate).getTime() - getToday().getTime(); return Math.max(1, Math.floor(diffMs / 86400000) + 1); } function formatEndDate(date) { return `${DAY_NAMES_SHORT[date.getDay()]} ${date.getDate()} ${MONTHS_SHORT[date.getMonth()]}`; } function formatHorizonLabel(date) { return sameDay(date, getToday()) ? 'Do dziś' : `Do ${formatEndDate(date)}`; } function formatRangeSummary(date) { return sameDay(date, getToday()) ? 'Zakres: tylko dziś' : `Zakres: dziś - ${formatEndDate(date)}`; } function formatDayContext(dayStrings) { const dayNames = dayStrings.map((ds) => { const d = new Date(ds + 'T00:00:00'); return DAY_NAMES_SHORT[d.getDay()]; }); if (dayNames.length <= 3) return dayNames.join(', '); return dayNames.slice(0, 3).join(', ') + ', \u2026'; } /* ── media helpers ── */ function photoStripMedia(image, icon, accentBg) { if (image) { return `
`; } return `
`; } /* ══════════════════════ HTML SHELL ══════════════════════ */ export function getPantryHTML() { return ` ${getIngredientCardHTML({ idBase: 'pv2-card' })}`; } /* ══════════════════════ HORIZON SELECTOR ══════════════════════ */ function syncHorizonUI() { ensureValidHorizonDate(); const defaultRow = document.getElementById('pantry-default-row'); const horizonExpanded = document.getElementById('pantry-horizon-expanded'); const searchShell = document.getElementById('pantry-search-shell'); const popover = document.getElementById('pantry-calendar-popover'); const filterPopover = document.getElementById('pantry-filter-popover'); const filterToggle = document.getElementById('pantry-filter-toggle'); const filterCount = document.getElementById('pantry-filter-count'); const compactLabel = document.getElementById('pantry-horizon-compact-label'); const horizonLabel = document.getElementById('pantry-horizon-label'); const chevron = document.getElementById('pantry-horizon-chevron'); const selectionEl = document.getElementById('pantry-cal-selection'); const prevBtn = document.getElementById('pantry-cal-prev'); const todayBtn = document.getElementById('pantry-cal-today'); if (compactLabel) compactLabel.textContent = formatHorizonLabel(horizonEndDate); if (horizonLabel) horizonLabel.textContent = formatHorizonLabel(horizonEndDate); if (selectionEl) selectionEl.textContent = formatRangeSummary(horizonEndDate); const showCalendar = isCalendarOpen && !isSearchExpanded; const showDefault = !isSearchExpanded && !isCalendarOpen; const showFilter = isFilterOpen && showDefault; const activeFilterCount = getActivePantryFilterCount(); if (defaultRow) { defaultRow.style.opacity = showDefault ? '1' : '0'; defaultRow.style.transform = showDefault ? 'translateY(0) scale(1)' : 'translateY(-2px) scale(0.98)'; defaultRow.style.pointerEvents = showDefault ? 'auto' : 'none'; } if (horizonExpanded) { horizonExpanded.style.opacity = showCalendar ? '1' : '0'; horizonExpanded.style.transform = showCalendar ? 'translateY(0) scale(1)' : 'translateY(-2px) scale(0.98)'; horizonExpanded.style.pointerEvents = showCalendar ? 'auto' : 'none'; } if (popover) { popover.style.opacity = showCalendar ? '1' : '0'; popover.style.transform = showCalendar ? 'translateY(0) scale(1)' : 'translateY(-6px) scale(0.98)'; popover.style.pointerEvents = showCalendar ? 'auto' : 'none'; } if (chevron) { chevron.style.transform = showCalendar ? 'rotate(180deg)' : 'rotate(0deg)'; } if (filterPopover) { filterPopover.style.opacity = showFilter ? '1' : '0'; filterPopover.style.transform = showFilter ? 'translateY(0) scale(1)' : 'translateY(-6px) scale(0.98)'; filterPopover.style.pointerEvents = showFilter ? 'auto' : 'none'; } if (filterToggle) { const isActive = showFilter || hasActivePantryFilters(); filterToggle.style.setProperty('background', isActive ? '#23221e' : '#393937', 'important'); filterToggle.style.setProperty('border-color', isActive ? '#787876' : '#41423f', 'important'); filterToggle.style.setProperty('color', isActive ? '#f2efe8' : '#ddd6ca', 'important'); } if (filterCount) { filterCount.textContent = String(activeFilterCount); filterCount.classList.toggle('hidden', activeFilterCount === 0); filterCount.classList.toggle('flex', activeFilterCount > 0); } if (prevBtn) { const isCurrentMonth = sameMonth(calendarMonthAnchor, getToday()); prevBtn.disabled = isCurrentMonth; prevBtn.style.opacity = isCurrentMonth ? '0.45' : '1'; prevBtn.style.cursor = isCurrentMonth ? 'default' : 'pointer'; } syncCalendarTodayButton( todayBtn, sameMonth(calendarMonthAnchor, getToday()), horizonEndDate, { ariaLabelGo: 'Pokaż bieżący miesiąc w kalendarzu', ariaLabelCurrent: 'Wyświetlany jest bieżący miesiąc', }, ); if (searchShell) { searchShell.style.opacity = isSearchExpanded ? '1' : '0'; searchShell.style.transform = isSearchExpanded ? 'translateY(0) scale(1)' : 'translateY(-2px) scale(0.98)'; searchShell.style.pointerEvents = isSearchExpanded ? 'auto' : 'none'; } renderCalendarPopover(); renderFilterPopover(); } function renderCalendarPopover() { const gridEl = document.getElementById('pantry-calendar-grid'); if (!gridEl) return; ensureValidHorizonDate(); const today = getToday(); const plans = loadPlans(); renderCalendarGrid({ gridEl, mode: 'month', anchorDate: calendarMonthAnchor, selectedDate: horizonEndDate, resolveDayState: (day, meta) => { const isPast = day.getTime() < today.getTime(); return { disabled: isPast, dimmed: isPast || !meta.inCurrentMonth, showIndicator: dayHasAnyMeal(plans, day), }; }, dayAttr: PANTRY_CALENDAR_DAY_ATTR, theme: PANTRY_CALENDAR_THEME, }); } function filterChipHtml(kind, value, label, active) { return ``; } function renderFilterPopover() { const body = document.getElementById('pantry-filter-panel-body'); if (!body) return; const categoryChips = CATEGORY_ORDER .map((category) => filterChipHtml( 'category', category, CATEGORY_LABELS[category] || category, pantryFilters.categories.includes(category), )) .join(''); const sectionChips = PANTRY_SECTION_FILTERS .map((item) => filterChipHtml( 'section', item.id, item.label, pantryFilters.sections.includes(item.id), )) .join(''); body.innerHTML = `

Kategorie

${categoryChips}

Sekcje

${sectionChips}
`; } function closeSearch() { const input = document.getElementById('pantry-search'); const hadQuery = Boolean(input?.value); if (input) { input.value = ''; input.blur(); } isSearchExpanded = false; syncHorizonUI(); if (hadQuery) renderBoard(); } function openSearch() { isSearchExpanded = true; isCalendarOpen = false; isFilterOpen = false; syncHorizonUI(); window.requestAnimationFrame(() => { document.getElementById('pantry-search')?.focus(); }); } function closeCalendar() { if (!isCalendarOpen) return; isCalendarOpen = false; syncHorizonUI(); } function openCalendar() { ensureValidHorizonDate(); calendarMonthAnchor = startOfMonth(horizonEndDate); isSearchExpanded = false; isFilterOpen = false; isCalendarOpen = true; syncHorizonUI(); } function closeFilter() { if (!isFilterOpen) return; isFilterOpen = false; syncHorizonUI(); } function toggleFilterPanel() { isCalendarOpen = false; isSearchExpanded = false; isFilterOpen = !isFilterOpen; syncHorizonUI(); } function selectHorizonDate(date) { const next = startOfDay(date); if (next.getTime() < getToday().getTime()) return; horizonEndDate = next; calendarMonthAnchor = startOfMonth(next); isCalendarOpen = false; syncHorizonUI(); renderBoard(); } /* ══════════════════════ DATA CLASSIFICATION ══════════════════════ */ /** * Classify all ingredients into 3 groups based on plan needs and pantry stock. */ function classifyIngredients(searchQuery) { const plans = loadPlans(); const pantry = loadPantry(); const today = getToday(); const needs = aggregateRangeIngredientNeed(plans, today, getHorizonDays()); const q = normalizeSearch(searchQuery); const matchesSearch = (name, category) => { if (!q) return true; return name.toLowerCase().includes(q) || (CATEGORY_LABELS[category] || '').toLowerCase().includes(q); }; const matchesCategory = (category) => pantryFilters.categories.length === 0 || pantryFilters.categories.includes(category); const matchesSection = (section) => pantryFilters.sections.length === 0 || pantryFilters.sections.includes(section); const neededIds = new Set(); const shortfalls = []; const sufficient = []; for (const need of needs) { neededIds.add(need.ingredientId); if (!matchesSearch(need.name, need.category) || !matchesCategory(need.category)) continue; const have = getPantryTotal(need.ingredientId, pantry); const def = INGREDIENTS[need.ingredientId]; const icon = CATEGORY_ICONS[def?.category] || 'fa-jar'; const item = { ingredientId: need.ingredientId, name: need.name, category: need.category, needed: need.amount, unit: need.unit, pantryQty: have, days: need.days, image: def?.image || null, icon, }; if (have < need.amount) { if (!matchesSection('shortfalls')) continue; shortfalls.push({ ...item, shortfall: Math.round((need.amount - have) * 10) / 10, fillPct: need.amount > 0 ? Math.min(100, Math.round((have / need.amount) * 100)) : 0, }); } else { if (!matchesSection('sufficient')) continue; sufficient.push({ ...item, fillPct: 100, }); } } // "Poza planem": all ingredients NOT in any plan need const notPlanned = Object.keys(INGREDIENTS) .filter((id) => !neededIds.has(id)) .filter(() => matchesSection('notPlanned')) .filter((id) => matchesSearch(INGREDIENTS[id].name, INGREDIENTS[id].category)) .filter((id) => matchesCategory(INGREDIENTS[id].category)) .map((id) => { const def = INGREDIENTS[id]; return { ingredientId: id, name: def.name, qty: getPantryTotal(id, pantry), unit: def.pantryUnit, icon: CATEGORY_ICONS[def.category] || 'fa-jar', }; }) .sort((a, b) => a.name.length - b.name.length); return { shortfalls, sufficient, notPlanned }; } /* ══════════════════════ TILE RENDERING ══════════════════════ */ function shortfallTileHtml(item) { const clamp = item.name.length > 25 ? ' min-w-0' : ''; return ` `; } function sufficientTileHtml(item) { const clamp = item.name.length > 25 ? ' min-w-0' : ''; return ` `; } function notPlannedChipHtml(item) { const hasStock = item.qty > 0; const clamp = item.name.length > 25 ? ' min-w-0' : ''; return ` `; } /* ══════════════════════ SECTION RENDERING ══════════════════════ */ function sectionHeaderHtml(icon, iconBg, iconColor, title, titleColor, count) { return `
${esc(title)} ${count}
`; } function renderBoard() { const root = document.getElementById('pantry-board'); if (!root) return; const q = document.getElementById('pantry-search')?.value || ''; const hasFilters = hasActivePantryFilters(); const { shortfalls, sufficient, notPlanned } = classifyIngredients(q); const totalVisible = shortfalls.length + sufficient.length + notPlanned.length; if (totalVisible === 0 && (q || hasFilters)) { root.innerHTML = `

Brak wyników — zmień filtry lub wyszukiwanie.

`; return; } let html = ''; // Section 1: Potrzebne (shortfalls) if (shortfalls.length > 0) { html += `
`; html += sectionHeaderHtml('fa-exclamation', '#2f2f2d', SHORTFALL_ACCENT, 'Potrzebne', '#7B756D', shortfalls.length); html += `
`; html += shortfalls.sort((a, b) => a.name.length - b.name.length).map(shortfallTileHtml).join(''); html += `
`; } // Section 2: W spiżarni (sufficient) if (sufficient.length > 0) { html += `
`; html += sectionHeaderHtml('fa-check', '#1a2e22', '#6ee7b7', 'W spiżarni', '#7B756D', sufficient.length); html += `
`; html += sufficient.sort((a, b) => a.name.length - b.name.length).map(sufficientTileHtml).join(''); html += `
`; } // Section 3: Poza planem if (notPlanned.length > 0) { html += `
`; html += sectionHeaderHtml('fa-minus', '#2a2a28', '#6d6c67', 'Poza planem', '#7B756D', notPlanned.length); html += `
`; html += notPlanned.map(notPlannedChipHtml).join(''); html += `
`; } // Empty state: no plans at all if (shortfalls.length === 0 && sufficient.length === 0 && !q && !hasFilters) { html = `

Brak zaplanowanych posiłków

Zaplanuj posiłki, a spiżarnia pokaże czego potrzebujesz i co masz na stanie.

` + html; } root.innerHTML = html; // Bind tile clicks root.querySelectorAll('.pv2-tile').forEach((btn) => { btn.addEventListener('click', () => openIngredientCard(btn.dataset.id, null)); }); } /* ══════════════════════ INGREDIENT SHEET ══════════════════════ */ function openIngredientCard(ingredientId, productId) { ingredientCard?.open({ ingredientId, productId, sourceNote: 'Ze spiżarni', onAfterChange: () => renderBoard(), }); } /* ══════════════════════ PUBLIC API ══════════════════════ */ export function refreshPantry() { syncHorizonUI(); renderBoard(); ingredientCard?.refresh(); } export function setupPantry() { if (!ingredientCard) { ingredientCard = createIngredientCardController({ idBase: 'pv2-card', defaultSourceNote: 'Ze spiżarni' }); ingredientCard.bind(); } syncHorizonUI(); renderBoard(); // Scroll shadow under top bar const pantryScroll = document.getElementById('pantry-scroll'); const topbarOuter = document.getElementById('pantry-topbar-outer'); if (pantryScroll && topbarOuter) { const shadow = document.createElement('div'); shadow.style.cssText = 'position:absolute;left:0;right:0;bottom:-8px;height:8px;background:linear-gradient(to bottom,rgba(0,0,0,0.25),transparent);opacity:0;transition:opacity 0.2s;pointer-events:none;'; topbarOuter.appendChild(shadow); pantryScroll.addEventListener('scroll', () => { shadow.style.opacity = pantryScroll.scrollTop > 2 ? '1' : '0'; }); } // Search document.getElementById('pantry-search')?.addEventListener('input', () => renderBoard()); document.getElementById('pantry-search')?.addEventListener('keydown', (event) => { if (event.key === 'Escape') closeSearch(); }); document.getElementById('pantry-search-toggle')?.addEventListener('click', () => openSearch()); document.getElementById('pantry-search-close')?.addEventListener('click', () => closeSearch()); document.getElementById('pantry-filter-toggle')?.addEventListener('click', () => toggleFilterPanel()); document.getElementById('pantry-filter-clear')?.addEventListener('click', () => { pantryFilters = { categories: [], sections: [] }; syncHorizonUI(); renderBoard(); }); document.getElementById('pantry-filter-panel-body')?.addEventListener('click', (event) => { const target = event.target; if (!(target instanceof Element)) return; const chip = target.closest('[data-pantry-filter-kind]'); if (!(chip instanceof Element)) return; const kind = chip.getAttribute('data-pantry-filter-kind'); const value = chip.getAttribute('data-pantry-filter-value'); if (!kind || !value) return; if (kind === 'category') { pantryFilters = { ...pantryFilters, categories: toggleStringFilter(pantryFilters.categories, value), }; } if (kind === 'section') { pantryFilters = { ...pantryFilters, sections: toggleStringFilter(pantryFilters.sections, value), }; } syncHorizonUI(); renderBoard(); }); // Horizon pill + calendar document.getElementById('pantry-horizon-compact')?.addEventListener('click', (event) => { event.stopPropagation(); openCalendar(); }); document.getElementById('pantry-horizon-toggle')?.addEventListener('click', () => { if (isCalendarOpen) { closeCalendar(); return; } openCalendar(); }); document.getElementById('pantry-cal-prev')?.addEventListener('click', () => { const prevMonth = addMonths(calendarMonthAnchor, -1); if (prevMonth.getTime() < startOfMonth(getToday()).getTime()) return; calendarMonthAnchor = prevMonth; syncHorizonUI(); }); document.getElementById('pantry-cal-next')?.addEventListener('click', () => { calendarMonthAnchor = addMonths(calendarMonthAnchor, 1); syncHorizonUI(); }); document.getElementById('pantry-cal-today')?.addEventListener('click', () => { calendarMonthAnchor = startOfMonth(getToday()); syncHorizonUI(); }); bindCalendarDayClicks(document.getElementById('pantry-calendar-grid'), (date) => { selectHorizonDate(date); }, PANTRY_CALENDAR_DAY_ATTR); if (!pantryGlobalListenersBound) { pantryGlobalListenersBound = true; document.addEventListener('click', (event) => { const target = event.target; if (!(target instanceof Element)) return; if (isCalendarOpen && !target.closest('#pantry-horizon-wrap, #pantry-horizon-compact')) { closeCalendar(); } if (isFilterOpen && !target.closest('#pantry-filter-wrap')) { closeFilter(); } }); } window.refreshPantry = refreshPantry; }