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