import { INGREDIENTS, RECIPES } from '../data/catalog.js?v=8';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
import {
addMonths,
addWeeks,
sameMonth,
startOfDay,
startOfMonth,
startOfWeekMonday,
weekContains,
} from '../services/dateUtils.js';
import {
computeEntryNutrition,
computeFullForecast,
countDayShortfalls,
dayHasAnyMeal,
sumDayNutrition,
} from '../services/planIngredients.js?v=4';
import { addOrMergeShoppingLines, loadPantry } from '../services/pantryShopping.js?v=2';
import {
dateKey,
getDayPlan,
loadPlans,
newPlanEntryId,
savePlans,
} from '../services/planStore.js?v=2';
import {
CALENDAR_HANDLE_CLASS,
CALENDAR_MONTHS_SHORT,
bindCalendarDayClicks,
createCalendarTopbarHTML,
createCalendarWeekdayHeaderHTML,
formatCalendarMonthYear,
formatCalendarSelectedDate,
isCalendarOnToday,
renderCollapsibleCalendar,
syncCalendarTodayButton,
syncCollapsibleCalendarMode,
} from '../ui/mealCalendar.js?v=1';
const WEEKDAYS_LONG = [
'Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota',
];
const PLANNER_SHEET_BOTTOM_INSET = '5.25rem';
const PLANNER_SHEET_MAX_HEIGHT = '70vh';
const PLANNER_SHEET_OFF_TRANSFORM = `translateY(calc(100% + ${PLANNER_SHEET_BOTTOM_INSET}))`;
function recipesForSlot(slotId) {
return Object.values(RECIPES).filter((r) => r.allowedSlots.includes(slotId));
}
function syncTodayButton(mode, weekStart, monthAnchor, selected) {
syncCalendarTodayButton(
document.getElementById('cal-go-today'),
isCalendarOnToday(mode, weekStart, monthAnchor, selected),
);
}
export function getMealPlannerHTML() {
return `
${createCalendarTopbarHTML({
titleId: 'cal-period-label',
prevId: 'cal-prev',
todayId: 'cal-go-today',
nextId: 'cal-next',
titleClass: 'text-[18px] font-semibold text-[#ddd6ca] leading-none tracking-[-0.03em]',
})}
${createCalendarWeekdayHeaderHTML()}
${createCalendarWeekdayHeaderHTML()}
`;
}
function updatePeriodLabel(mode, weekStart, monthAnchor, selected) {
const el = document.getElementById('cal-period-label');
if (!el) return;
if (mode === 'week') {
el.textContent = formatCalendarSelectedDate(selected);
} else {
el.textContent = formatCalendarMonthYear(monthAnchor);
}
}
function syncModeToggle(mode) {
syncCollapsibleCalendarMode({
mode,
weekWrapEl: document.getElementById('calendar-week-wrap'),
monthWrapEl: document.getElementById('calendar-month-wrap'),
handleEl: document.getElementById('calendar-handle-icon'),
});
}
function bindCalendarSwipeGesture(state, rerender) {
const zone = document.getElementById('calendar-swipe-zone');
if (!zone) return;
let startY = 0;
let ptrId = null;
let moved = false;
zone.addEventListener('pointerdown', (e) => {
if (ptrId !== null) return;
startY = e.clientY;
ptrId = e.pointerId;
moved = false;
});
zone.addEventListener('pointermove', (e) => {
if (e.pointerId !== ptrId) return;
if (Math.abs(e.clientY - startY) > 10) moved = true;
});
zone.addEventListener('pointerup', (e) => {
if (e.pointerId !== ptrId) return;
const dy = e.clientY - startY;
ptrId = null;
if (!moved || Math.abs(dy) < 30) return;
let switched = false;
if (state.mode === 'week' && dy > 30) {
state.mode = 'month';
state.monthAnchor = startOfMonth(state.selected);
switched = true;
} else if (state.mode === 'month' && dy < -30) {
state.mode = 'week';
state.weekStart = startOfWeekMonday(state.selected);
switched = true;
}
if (switched) {
zone.addEventListener('click', (ev) => {
ev.stopPropagation();
ev.preventDefault();
}, { capture: true, once: true });
rerender();
}
});
zone.addEventListener('pointercancel', () => {
ptrId = null;
moved = false;
});
}
function showPlannerToast(message) {
const wrap = document.getElementById('planner-toast');
const text = document.getElementById('planner-toast-text');
if (!wrap || !text) return;
text.textContent = message;
wrap.classList.remove('opacity-0', 'translate-y-2');
wrap.classList.add('opacity-100', 'translate-y-0');
clearTimeout(showPlannerToast._t);
showPlannerToast._t = setTimeout(() => {
wrap.classList.add('opacity-0', 'translate-y-2');
wrap.classList.remove('opacity-100', 'translate-y-0');
}, 2600);
}
function openSheet(backdrop, sheet) {
if (!backdrop || !sheet) return;
sheet.style.visibility = 'visible';
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
sheet.style.transform = 'translateY(0)';
backdrop.classList.remove('hidden');
requestAnimationFrame(() => {
backdrop.classList.remove('opacity-0');
});
}
function closeSheet(backdrop, sheet) {
if (!backdrop || !sheet) return;
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
sheet.style.transform = PLANNER_SHEET_OFF_TRANSFORM;
backdrop.classList.add('opacity-0');
setTimeout(() => {
backdrop.classList.add('hidden');
sheet.style.visibility = 'hidden';
}, 300);
}
/** Zamykanie panelu: przeciągnięcie nagłówka w dół (pointer). */
function bindPlannerSheetDragClose(sheet, closeFn) {
const zone = sheet.querySelector('[data-planner-sheet-drag-zone]');
if (!zone || !sheet) return;
let startY = 0;
let pulling = false;
let ptrId = null;
const resetVisual = () => {
sheet.style.transition = 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)';
sheet.style.transform = 'translateY(0)';
};
zone.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'mouse' && e.button !== 0) return;
pulling = true;
ptrId = e.pointerId;
startY = e.clientY;
sheet.style.transition = 'none';
zone.setPointerCapture(e.pointerId);
});
zone.addEventListener('pointermove', (e) => {
if (!pulling || e.pointerId !== ptrId) return;
const dy = Math.max(0, e.clientY - startY);
sheet.style.transform = `translateY(${dy}px)`;
});
zone.addEventListener('pointerup', (e) => {
if (!pulling || e.pointerId !== ptrId) return;
const dy = e.clientY - startY;
pulling = false;
ptrId = null;
try {
zone.releasePointerCapture(e.pointerId);
} catch {
/* ignore */
}
if (dy > 56) {
closeFn();
return;
}
resetVisual();
});
zone.addEventListener('pointercancel', () => {
pulling = false;
ptrId = null;
resetVisual();
});
}
function renderDayContent(state) {
const sel = state.selected;
const dayPlan = getDayPlan(state.plans, sel);
const totals = sumDayNutrition(dayPlan);
const setText = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = value;
};
const setGrams = (id, value) => {
const el = document.getElementById(id);
if (!el) return;
el.textContent = value === null ? '—' : `${value}g`;
};
const hasMeals = totals.mealCount > 0;
setText('planner-nutrition-kcal', hasMeals ? String(totals.kcal) : '—');
setGrams('planner-nutrition-p', hasMeals ? totals.protein : null);
setGrams('planner-nutrition-f', hasMeals ? totals.fat : null);
setGrams('planner-nutrition-c', hasMeals ? totals.carbs : null);
const ingBtn = document.getElementById('planner-open-ingredients');
if (ingBtn) {
const noMeals = totals.mealCount === 0;
ingBtn.disabled = noMeals;
ingBtn.classList.toggle('opacity-50', noMeals);
ingBtn.classList.toggle('cursor-not-allowed', noMeals);
if (!noMeals) {
const shortCount = countDayShortfalls(dayPlan, loadPantry());
if (shortCount > 0) {
ingBtn.innerHTML = `
Składniki na ten dzień
${shortCount}`;
} else {
ingBtn.innerHTML = `
Składniki na ten dzień
OK`;
}
} else {
ingBtn.innerHTML = ` Składniki na ten dzień`;
}
}
const slotsRoot = document.getElementById('planner-meal-slots');
if (!slotsRoot) return;
const skipped = dayPlan._skipped || {};
slotsRoot.innerHTML = MEAL_SLOTS.map((slot) => {
const isSkipped = skipped[slot.id] === true;
const entries = isSkipped ? [] : (Array.isArray(dayPlan[slot.id]) ? dayPlan[slot.id] : []);
let slotKcal = 0;
entries.forEach((entry) => {
if (entry?.recipeId && RECIPES[entry.recipeId]) slotKcal += computeEntryNutrition(entry).kcal;
});
const entryCards = entries.map((entry) => {
const recipe = entry && entry.recipeId ? RECIPES[entry.recipeId] : null;
if (!recipe) return '';
const servings = Math.max(1, Number(entry.servings) || 1);
const entryN = computeEntryNutrition(entry);
const eid = escapeHtml(entry.id);
const hasCustom = (entry.excludedIngredients?.length > 0) ||
(entry.amountOverrides && Object.keys(entry.amountOverrides).length > 0) ||
(entry.addedIngredients?.length > 0) ||
(entry.substitutions && Object.keys(entry.substitutions).length > 0);
const customDot = hasCustom ? '' : '';
const servLabel = servings > 1 ? `·×${servings}` : '';
return `
${recipe.image
? `
})
`
: `
${escapeHtml(recipe.thumbLabel)}`}
${escapeHtml(recipe.title)}
${customDot}
${recipe.minutes} min
·
${entryN.kcal} kcal${servLabel}
`;
}).join('');
const addBtn = ``;
const kcalPill = slotKcal > 0
? `${slotKcal} kcal`
: '';
const filledCard = `
${slot.label}
${kcalPill}
${addBtn}
${entries.length > 0 ? `
${entryCards}
` : ''}
`;
if (entries.length > 0) return filledCard;
return `
`;
}).join('');
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function getRecentRecipeIds(plans, limit = 5) {
const seen = new Map();
const keys = Object.keys(plans).sort().reverse();
for (const key of keys) {
const day = plans[key];
if (!day) continue;
for (const slotId of Object.keys(day)) {
if (slotId === '_skipped') continue;
const entries = day[slotId];
if (!Array.isArray(entries)) continue;
for (const e of entries) {
if (e?.recipeId && RECIPES[e.recipeId] && !seen.has(e.recipeId)) {
seen.set(e.recipeId, true);
if (seen.size >= limit) return [...seen.keys()];
}
}
}
}
return [...seen.keys()];
}
function recipeCardHtml(r) {
return `
`;
}
let _pickerSlotRecipes = [];
let _pickerPlans = {};
function renderPickerList(slotId, plans, query = '') {
const slot = MEAL_SLOTS.find((s) => s.id === slotId);
const list = document.getElementById('planner-picker-list');
const title = document.getElementById('planner-picker-title');
const sub = document.getElementById('planner-picker-sub');
if (!list || !title || !sub) return;
title.textContent = 'Wybierz przepis';
sub.textContent = slot ? `Dla: ${slot.label}` : '';
const allRecipes = recipesForSlot(slotId);
_pickerSlotRecipes = allRecipes;
_pickerPlans = plans;
const q = query.trim().toLowerCase();
const filtered = q
? allRecipes.filter((r) => r.title.toLowerCase().includes(q) || (r.tags || []).some((t) => t.toLowerCase().includes(q)))
: allRecipes;
if (filtered.length === 0 && q) {
list.innerHTML = 'Brak wyników.
';
return;
}
if (filtered.length === 0) {
list.innerHTML = 'Brak dopasowanych przepisów.
';
return;
}
let html = '';
if (!q) {
const recentIds = getRecentRecipeIds(plans);
const recentInSlot = recentIds.map((id) => RECIPES[id]).filter((r) => r && r.allowedSlots.includes(slotId));
if (recentInSlot.length > 0) {
html += `Ostatnio używane
`;
html += recentInSlot.map(recipeCardHtml).join('');
html += ``;
html += `Wszystkie
`;
}
}
html += filtered.map(recipeCardHtml).join('');
list.innerHTML = html;
}
function plIngredientWord(n) {
if (n === 1) return 'składnik';
const m10 = n % 10;
const m100 = n % 100;
if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return 'składniki';
return 'składników';
}
function updateIngButtons(state) {
const btn1 = document.getElementById('planner-ing-add-all');
const btn2 = document.getElementById('planner-ing-add-btn');
const todayCount = (state._todayShortfalls || []).length;
const allCount = (state._allForecastShortfalls || []).length;
if (btn1) {
if (todayCount > 0) {
btn1.classList.remove('hidden');
btn1.disabled = false;
btn1.innerHTML = ` Dodaj braki na dziś do listy`;
} else {
btn1.classList.add('hidden');
}
}
if (btn2) {
if (allCount > todayCount) {
btn2.classList.remove('hidden');
btn2.innerHTML = ` Dodaj braki na cały tydzień`;
} else {
btn2.classList.add('hidden');
}
}
}
function renderIngredientsSheet(state) {
const body = document.getElementById('planner-ing-body');
const titleEl = document.getElementById('planner-ing-title');
const subEl = document.getElementById('planner-ing-sub');
if (!body) return;
const pantry = loadPantry();
const forecast = computeFullForecast(state.plans, pantry, state.selected);
const today = forecast.length > 0 && forecast[0].dayIndex === 0 ? forecast[0] : null;
const upcoming = forecast.filter((d) => d.dayIndex > 0 && d.hasShortfall);
state._todayShortfalls = today ? today.items.filter((it) => !it.enough) : [];
state._allForecastShortfalls = [];
for (const d of forecast) {
for (const it of d.items) {
if (!it.enough) state._allForecastShortfalls.push(it);
}
}
if (titleEl) {
const wd = WEEKDAYS_LONG[state.selected.getDay()];
titleEl.textContent = `${wd}, ${state.selected.getDate()} ${CALENDAR_MONTHS_SHORT[state.selected.getMonth()]} — składniki`;
}
if (subEl) subEl.textContent = 'Porównanie potrzeb z zapasami w spiżarni.';
if (!today || today.items.length === 0) {
body.innerHTML = 'Najpierw zaplanuj posiłki.
';
updateIngButtons(state);
return;
}
const shortItems = today.items.filter((it) => !it.enough);
const okItems = today.items.filter((it) => it.enough);
let html = '';
if (shortItems.length === 0) {
html += `
Wszystko masz w spiżarni
${today.items.length} ${plIngredientWord(today.items.length)} — zapasy wystarczą
`;
} else {
html += `
${shortItems.length} ${plIngredientWord(shortItems.length)} do kupienia
Brakuje składników na zaplanowane posiłki
`;
}
if (shortItems.length > 0) {
html += `
Do kupienia
${shortItems.map((ing) => `
-
${escapeHtml(ing.name)}
potrzeba ${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}
·
w spiżarni ${ing.pantryQty > 0 ? formatAmount(ing.pantryQty) + ' ' + escapeHtml(ing.pantryUnit) : 'brak'}
−${formatAmount(ing.shortfall)}
${escapeHtml(ing.pantryUnit)}
`).join('')}
`;
}
if (okItems.length > 0) {
html += `
W spiżarni
${okItems.map((ing) => `
-
${escapeHtml(ing.name)}
potrzeba ${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}
·
masz ${formatAmount(ing.pantryQty)} ${escapeHtml(ing.pantryUnit)}
`).join('')}
`;
}
if (upcoming.length > 0) {
html += `
Nadchodzące braki
${upcoming.map((day) => {
const wd = WEEKDAYS_LONG[day.date.getDay()];
const label = `${wd}, ${day.date.getDate()} ${CALENDAR_MONTHS_SHORT[day.date.getMonth()]}`;
const shorts = day.items.filter((it) => !it.enough);
return `
${escapeHtml(label)}
${shorts.map((it) => `
-
${escapeHtml(it.name)}
−${formatAmount(it.shortfall)} ${escapeHtml(it.pantryUnit)}
`).join('')}
`;
}).join('')}
`;
}
body.innerHTML = html;
updateIngButtons(state);
}
function formatAmount(n) {
return Number.isInteger(n) ? String(n) : String(n);
}
function seedDemoIfEmpty(plans) {
const todayKey = dateKey(new Date());
if (Object.keys(plans).length > 0) return plans;
return {
...plans,
[todayKey]: {
sniadanie: [{ id: newPlanEntryId(), recipeId: 'jajecznica', servings: 1 }],
obiad: [{ id: newPlanEntryId(), recipeId: 'makaron_ricotta', servings: 1 }],
kolacja: [{ id: newPlanEntryId(), recipeId: 'kanapka_losos', servings: 1 }],
},
};
}
export function setupMealPlanner() {
let plans = loadPlans();
plans = seedDemoIfEmpty(plans);
savePlans(plans);
const state = {
mode: 'week',
weekStart: startOfWeekMonday(new Date()),
monthAnchor: startOfDay(new Date()),
selected: startOfDay(new Date()),
plans,
pickerSlot: null,
};
const weekGrid = document.getElementById('calendar-week-grid');
const monthGrid = document.getElementById('calendar-month-grid');
const pickerBackdrop = document.getElementById('planner-picker-backdrop');
const pickerSheet = document.getElementById('planner-picker-sheet');
const ingBackdrop = document.getElementById('planner-ing-backdrop');
const ingSheet = document.getElementById('planner-ing-sheet');
const rerender = () => {
syncModeToggle(state.mode);
updatePeriodLabel(state.mode, state.weekStart, state.monthAnchor, state.selected);
syncTodayButton(state.mode, state.weekStart, state.monthAnchor, state.selected);
renderCollapsibleCalendar({
weekGridEl: weekGrid,
monthGridEl: monthGrid,
weekAnchorDate: state.weekStart,
monthAnchorDate: state.monthAnchor,
selectedDate: state.selected,
resolveDayState: (day, meta) => ({
dimmed: meta.mode === 'month' && !meta.inCurrentMonth,
showIndicator: meta.mode === 'month'
? meta.inCurrentMonth && dayHasAnyMeal(state.plans, day)
: dayHasAnyMeal(state.plans, day),
}),
});
renderDayContent(state);
};
const persist = () => {
savePlans(state.plans);
rerender();
};
/* ── calendar scroll shadow ─────────────────── */
const plannerScroll = document.getElementById('planner-scroll');
const calBar = document.getElementById('planner-cal-bar');
if (plannerScroll && calBar) {
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;';
calBar.appendChild(shadow);
plannerScroll.addEventListener('scroll', () => {
shadow.style.opacity = plannerScroll.scrollTop > 2 ? '1' : '0';
});
}
bindCalendarDayClicks(weekGrid, (date) => {
state.selected = date;
rerender();
});
bindCalendarDayClicks(monthGrid, (date) => {
state.selected = date;
rerender();
});
document.getElementById('cal-prev')?.addEventListener('click', () => {
if (state.mode === 'week') {
state.weekStart = addWeeks(state.weekStart, -1);
if (!weekContains(state.weekStart, state.selected)) {
state.selected = new Date(state.weekStart);
}
} else {
state.monthAnchor = addMonths(state.monthAnchor, -1);
if (!sameMonth(state.monthAnchor, state.selected)) {
state.selected = startOfMonth(state.monthAnchor);
}
}
rerender();
});
document.getElementById('cal-next')?.addEventListener('click', () => {
if (state.mode === 'week') {
state.weekStart = addWeeks(state.weekStart, 1);
if (!weekContains(state.weekStart, state.selected)) {
state.selected = new Date(state.weekStart);
}
} else {
state.monthAnchor = addMonths(state.monthAnchor, 1);
if (!sameMonth(state.monthAnchor, state.selected)) {
state.selected = startOfMonth(state.monthAnchor);
}
}
rerender();
});
document.getElementById('cal-go-today')?.addEventListener('click', () => {
const today = startOfDay(new Date());
state.selected = today;
state.weekStart = startOfWeekMonday(today);
state.monthAnchor = startOfMonth(today);
rerender();
});
document.getElementById('planner-meal-slots')?.addEventListener('click', (e) => {
const skipBtn = e.target.closest('.planner-skip-meal');
if (skipBtn) {
const slotId = skipBtn.getAttribute('data-slot-id');
if (!slotId) return;
const key = dateKey(state.selected);
if (!state.plans[key]) state.plans[key] = {};
if (!state.plans[key]._skipped) state.plans[key]._skipped = {};
state.plans[key]._skipped[slotId] = true;
persist();
return;
}
const unskipBtn = e.target.closest('.planner-unskip');
if (unskipBtn) {
const slotId = unskipBtn.getAttribute('data-slot-id');
if (!slotId) return;
const key = dateKey(state.selected);
if (state.plans[key]?._skipped) {
delete state.plans[key]._skipped[slotId];
if (Object.keys(state.plans[key]._skipped).length === 0) delete state.plans[key]._skipped;
if (Object.keys(state.plans[key]).length === 0) delete state.plans[key];
}
persist();
return;
}
const openRecipe = e.target.closest('.planner-open-recipe');
if (openRecipe) {
const recipeId = openRecipe.getAttribute('data-recipe-id');
if (recipeId && window.openRecipeDetail) {
window.openRecipeDetail(recipeId);
}
return;
}
const addBtn = e.target.closest('.planner-add-meal');
if (addBtn) {
const slotId = addBtn.getAttribute('data-slot-id');
state.pickerSlot = slotId;
const searchInput = document.getElementById('planner-picker-search');
if (searchInput) searchInput.value = '';
renderPickerList(slotId, state.plans);
openSheet(pickerBackdrop, pickerSheet);
return;
}
const editBtn = e.target.closest('.planner-edit-meal');
if (editBtn) {
const slotId = editBtn.getAttribute('data-slot-id');
const entryId = editBtn.getAttribute('data-entry-id');
const key = dateKey(state.selected);
const arr = state.plans[key]?.[slotId];
if (!Array.isArray(arr)) return;
const entry = arr.find((x) => x && x.id === entryId);
if (!entry) return;
window.openMealPlanEditor?.({
mode: 'edit',
recipeId: entry.recipeId,
date: state.selected,
slotId,
entry,
});
return;
}
const clearBtn = e.target.closest('.planner-clear-meal');
if (clearBtn) {
const slotId = clearBtn.getAttribute('data-slot-id');
const entryId = clearBtn.getAttribute('data-entry-id');
const key = dateKey(state.selected);
const arr = state.plans[key]?.[slotId];
if (!Array.isArray(arr) || !entryId) return;
const next = arr.filter((x) => x && x.id !== entryId);
if (!state.plans[key]) state.plans[key] = {};
if (next.length === 0) delete state.plans[key][slotId];
else state.plans[key][slotId] = next;
if (Object.keys(state.plans[key]).length === 0) delete state.plans[key];
persist();
return;
}
});
const closePicker = () => {
state.pickerSlot = null;
closeSheet(pickerBackdrop, pickerSheet);
};
document.getElementById('planner-picker-search')?.addEventListener('input', (e) => {
if (state.pickerSlot) {
renderPickerList(state.pickerSlot, state.plans, e.target.value);
}
});
bindPlannerSheetDragClose(pickerSheet, closePicker);
bindPlannerSheetDragClose(ingSheet, () => closeSheet(ingBackdrop, ingSheet));
pickerBackdrop?.addEventListener('click', closePicker);
document.getElementById('planner-picker-list')?.addEventListener('click', (e) => {
const pick = e.target.closest('.planner-pick-recipe');
if (!pick || !state.pickerSlot) return;
const recipeId = pick.getAttribute('data-recipe-id');
if (!recipeId || !RECIPES[recipeId]) return;
const slotId = state.pickerSlot;
closePicker();
setTimeout(() => {
window.openMealPlanEditor?.({
mode: 'add',
recipeId,
date: state.selected,
slotId,
});
}, 320);
});
document.getElementById('planner-open-ingredients')?.addEventListener('click', () => {
if (sumDayNutrition(getDayPlan(state.plans, state.selected)).mealCount === 0) return;
renderIngredientsSheet(state);
openSheet(ingBackdrop, ingSheet);
});
ingBackdrop?.addEventListener('click', () => {
closeSheet(ingBackdrop, ingSheet);
});
ingSheet?.addEventListener('click', (e) => {
const row = e.target.closest('.planner-ing-row');
if (!row || !ingSheet.contains(row)) return;
row.classList.toggle('ingredient-active');
});
document.getElementById('planner-ing-add-all')?.addEventListener('click', () => {
const items = state._todayShortfalls || [];
if (items.length === 0) return;
const lines = items.map((it) => ({
ingredientId: it.ingredientId,
amount: it.shortfall,
unit: it.pantryUnit,
category: it.category,
sourceNote: 'Braki z planu dnia',
}));
addOrMergeShoppingLines(lines);
showPlannerToast(`Dodano ${lines.length} braków na listę zakupów.`);
window.refreshShopping?.();
closeSheet(ingBackdrop, ingSheet);
});
document.getElementById('planner-ing-add-btn')?.addEventListener('click', () => {
const items = state._allForecastShortfalls || [];
if (items.length === 0) return;
const map = new Map();
for (const it of items) {
const key = it.ingredientId;
if (map.has(key)) {
const cur = map.get(key);
cur.amount = Math.round((cur.amount + it.shortfall) * 10) / 10;
} else {
map.set(key, {
ingredientId: it.ingredientId,
amount: it.shortfall,
unit: it.pantryUnit,
category: it.category,
sourceNote: 'Braki z planu tygodnia',
});
}
}
const lines = [...map.values()];
addOrMergeShoppingLines(lines);
showPlannerToast(`Dodano ${lines.length} braków na listę zakupów.`);
window.refreshShopping?.();
closeSheet(ingBackdrop, ingSheet);
});
rerender();
window.refreshPlanner = () => {
state.plans = loadPlans();
rerender();
};
bindCalendarSwipeGesture(state, rerender);
requestAnimationFrame(() => {
const ww = document.getElementById('calendar-week-wrap');
const mw = document.getElementById('calendar-month-wrap');
const t = 'max-height 300ms ease, opacity 200ms ease';
if (ww) ww.style.transition = t;
if (mw) mw.style.transition = t;
});
}