const MONTHS_SHORT = [ 'sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', 'sie', 'wrz', 'paź', 'lis', 'gru', ]; const WEEKDAYS_SHORT = ['pn', 'wt', 'śr', 'cz', 'pt', 'so', 'nd']; const WEEKDAYS_LONG = [ 'Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota', ]; const MEAL_SLOTS = [ { id: 'sniadanie', label: 'Śniadanie', icon: 'fa-sun' }, { id: 'drugie_sniadanie', label: 'Drugie śniadanie', icon: 'fa-coffee' }, { id: 'obiad', label: 'Obiad', icon: 'fa-utensils' }, { id: 'przekaska', label: 'Przekąska', icon: 'fa-apple-alt' }, { id: 'kolacja', label: 'Kolacja', icon: 'fa-moon' }, ]; /** Katalog przepisów (spójny z listą w aplikacji) — porcja bazowa = 1 */ const PLANNER_RECIPES = { placki: { id: 'placki', title: 'Puszyste placki', minutes: 15, thumbLabel: 'Placki', allowedSlots: ['sniadanie', 'drugie_sniadanie'], nutritionPerServing: { kcal: 320, protein: 12, fat: 8, carbs: 48 }, ingredients: [ { name: 'Mąka pszenna', amount: 200, unit: 'g' }, { name: 'Mleko', amount: 250, unit: 'ml' }, { name: 'Jajka', amount: 2, unit: 'szt.' }, ], }, salatka: { id: 'salatka', title: 'Sałatka z kurczakiem', minutes: 20, thumbLabel: 'Sałatka', allowedSlots: ['obiad'], nutritionPerServing: { kcal: 250, protein: 35, fat: 9, carbs: 12 }, ingredients: [ { name: 'Pierś z kurczaka', amount: 150, unit: 'g' }, { name: 'Mix sałat', amount: 100, unit: 'g' }, { name: 'Pomidor', amount: 1, unit: 'szt.' }, ], }, makaron: { id: 'makaron', title: 'Makaron z pomidorami i bazylią', minutes: 30, thumbLabel: 'Makaron', allowedSlots: ['obiad', 'kolacja'], nutritionPerServing: { kcal: 450, protein: 14, fat: 12, carbs: 72 }, ingredients: [ { name: 'Makaron', amount: 120, unit: 'g' }, { name: 'Pomidory krojone', amount: 400, unit: 'g' }, { name: 'Bazylia świeża', amount: 10, unit: 'g' }, ], }, koktajl: { id: 'koktajl', title: 'Koktajl owocowy', minutes: 5, thumbLabel: 'Koktajl', allowedSlots: ['przekaska', 'drugie_sniadanie'], nutritionPerServing: { kcal: 180, protein: 8, fat: 3, carbs: 32 }, ingredients: [ { name: 'Jogurt naturalny', amount: 200, unit: 'g' }, { name: 'Mieszanka jagód', amount: 150, unit: 'g' }, { name: 'Miód', amount: 15, unit: 'g' }, ], }, tost_awokado: { id: 'tost_awokado', title: 'Tost z awokado', minutes: 10, thumbLabel: 'Tost', allowedSlots: ['sniadanie', 'drugie_sniadanie'], nutritionPerServing: { kcal: 220, protein: 6, fat: 14, carbs: 20 }, ingredients: [ { name: 'Chleb na zakwasie', amount: 2, unit: 'kromki' }, { name: 'Awokado', amount: 1, unit: 'szt.' }, { name: 'Cytryna', amount: 0.5, unit: 'szt.' }, ], }, losos: { id: 'losos', title: 'Grillowany łosoś', minutes: 25, thumbLabel: 'Łosoś', allowedSlots: ['kolacja', 'obiad'], nutritionPerServing: { kcal: 380, protein: 38, fat: 22, carbs: 4 }, ingredients: [ { name: 'Filet z łososia', amount: 180, unit: 'g' }, { name: 'Cytryna', amount: 0.5, unit: 'szt.' }, { name: 'Koper', amount: 5, unit: 'g' }, ], }, tacos: { id: 'tacos', title: 'Tacos z wołowiną', minutes: 20, thumbLabel: 'Tacos', allowedSlots: ['kolacja', 'obiad'], nutritionPerServing: { kcal: 410, protein: 28, fat: 18, carbs: 38 }, ingredients: [ { name: 'Mięso mielone wołowe', amount: 200, unit: 'g' }, { name: 'Tortille kukurydziane', amount: 4, unit: 'szt.' }, { name: 'Salsa pomidorowa', amount: 100, unit: 'g' }, ], }, owsianka: { id: 'owsianka', title: 'Miska owsianki', minutes: 10, thumbLabel: 'Owsianka', allowedSlots: ['sniadanie', 'drugie_sniadanie'], nutritionPerServing: { kcal: 210, protein: 8, fat: 6, carbs: 34 }, ingredients: [ { name: 'Płatki owsiane', amount: 60, unit: 'g' }, { name: 'Mleko', amount: 200, unit: 'ml' }, { name: 'Miód', amount: 20, unit: 'g' }, ], }, serek_owoc: { id: 'serek_owoc', title: 'Serek wiejski z orzechami i owocami', minutes: 5, thumbLabel: 'Serek', allowedSlots: ['sniadanie', 'drugie_sniadanie', 'przekaska'], nutritionPerServing: { kcal: 642, protein: 32, fat: 43, carbs: 41 }, ingredients: [ { name: 'Serek wiejski', amount: 200, unit: 'g' }, { name: 'Miód', amount: 10, unit: 'g' }, { name: 'Orzechy włoskie', amount: 50, unit: 'g' }, { name: 'Truskawki', amount: 100, unit: 'g' }, { name: 'Borówki ameryk.', amount: 80, unit: 'g' }, ], }, }; const PLANS_STORAGE_KEY = 'recipe-planner-plans-v1'; /** Odstęp od dołu planera = miejsce na dolną nawigację. Ten sam w `bottom`, `max-height` i w `translateY(calc(100% + …))` przy zamknięciu — inaczej zostaje widoczny uchwyt. */ const PLANNER_SHEET_BOTTOM_INSET = '5.25rem'; const PLANNER_SHEET_OFF_TRANSFORM = `translateY(calc(100% + ${PLANNER_SHEET_BOTTOM_INSET}))`; function startOfDay(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); return x; } function sameDay(a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); } function addDays(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return startOfDay(x); } /** Poniedziałek jako pierwszy dzień tygodnia (PL) */ function startOfWeekMonday(d) { const date = startOfDay(d); const day = date.getDay(); const diff = day === 0 ? -6 : 1 - day; return addDays(date, diff); } function startOfMonth(d) { const x = new Date(d.getFullYear(), d.getMonth(), 1); return startOfDay(x); } function addMonths(d, n) { const x = new Date(d); x.setMonth(x.getMonth() + n); return startOfDay(x); } function addWeeks(d, n) { return addDays(d, n * 7); } function weekContains(weekStart, d) { const t = startOfDay(d).getTime(); const ws = weekStart.getTime(); const we = addDays(weekStart, 6).getTime(); return t >= ws && t <= we; } function sameMonth(a, b) { return a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear(); } function dateKey(d) { const x = startOfDay(d); const y = x.getFullYear(); const m = String(x.getMonth() + 1).padStart(2, '0'); const day = String(x.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } function newPlanEntryId() { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } return `e${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; } /** Jedna pora dnia = tablica wpisów { id, recipeId, servings } (stary format: jeden obiekt — migrujemy przy wczytaniu). */ function normalizeSlotValue(v) { if (!v) return []; if (Array.isArray(v)) { return v .filter((x) => x && x.recipeId && PLANNER_RECIPES[x.recipeId]) .map((x) => ({ id: x.id && String(x.id).length ? String(x.id) : newPlanEntryId(), recipeId: x.recipeId, servings: Math.max(1, Math.min(12, Number(x.servings) || 1)), })); } if (typeof v === 'object' && v.recipeId && PLANNER_RECIPES[v.recipeId]) { return [{ id: newPlanEntryId(), recipeId: v.recipeId, servings: Math.max(1, Math.min(12, Number(v.servings) || 1)), }]; } return []; } function normalizeDayPlan(day) { if (!day || typeof day !== 'object') return {}; const out = {}; MEAL_SLOTS.forEach((s) => { const arr = normalizeSlotValue(day[s.id]); if (arr.length > 0) out[s.id] = arr; }); return out; } function normalizeAllPlans(plans) { if (!plans || typeof plans !== 'object') return {}; const out = {}; Object.keys(plans).forEach((key) => { const d = normalizeDayPlan(plans[key]); if (Object.keys(d).length > 0) out[key] = d; }); return out; } function loadPlans() { try { const raw = localStorage.getItem(PLANS_STORAGE_KEY); if (!raw) return {}; const parsed = JSON.parse(raw); if (typeof parsed !== 'object' || parsed === null) return {}; return normalizeAllPlans(parsed); } catch { return {}; } } function savePlans(plans) { try { localStorage.setItem(PLANS_STORAGE_KEY, JSON.stringify(plans)); } catch { /* ignore */ } } function getDayPlan(plans, d) { const key = dateKey(d); const day = plans[key]; return day && typeof day === 'object' ? day : {}; } function dayHasAnyMeal(plans, d) { const p = getDayPlan(plans, d); return MEAL_SLOTS.some((s) => { const arr = p[s.id]; return Array.isArray(arr) && arr.length > 0; }); } function sumDayNutrition(dayPlan) { let kcal = 0; let protein = 0; let fat = 0; let carbs = 0; let mealCount = 0; MEAL_SLOTS.forEach((slot) => { const entries = dayPlan[slot.id]; if (!Array.isArray(entries)) return; entries.forEach((entry) => { if (!entry || !entry.recipeId) return; const r = PLANNER_RECIPES[entry.recipeId]; if (!r) return; const s = Math.max(1, Number(entry.servings) || 1); mealCount += 1; kcal += r.nutritionPerServing.kcal * s; protein += r.nutritionPerServing.protein * s; fat += r.nutritionPerServing.fat * s; carbs += r.nutritionPerServing.carbs * s; }); }); return { kcal: Math.round(kcal), protein: Math.round(protein * 10) / 10, fat: Math.round(fat * 10) / 10, carbs: Math.round(carbs * 10) / 10, mealCount, }; } /** * Jedna grupa na porę dnia: nagłówek pory raz, potem bloki przepisów ze składnikami. * @returns {{ mealLabel: string, recipes: { recipeTitle: string, items: { name: string, amount: number, unit: string }[] }[] }[]} */ function aggregateDayIngredientsBySlot(dayPlan) { /** @type {{ mealLabel: string, recipes: { recipeTitle: string, items: { name: string, amount: number, unit: string }[] }[] }[]} */ const blocks = []; MEAL_SLOTS.forEach((slot) => { const entries = dayPlan[slot.id]; if (!Array.isArray(entries) || entries.length === 0) return; const recipes = []; entries.forEach((entry) => { if (!entry || !entry.recipeId) return; const r = PLANNER_RECIPES[entry.recipeId]; if (!r) return; const s = Math.max(1, Number(entry.servings) || 1); const items = r.ingredients.map((ing) => ({ name: ing.name, amount: Math.round(ing.amount * s * 10) / 10, unit: ing.unit, })); recipes.push({ recipeTitle: r.title, items }); }); if (recipes.length > 0) { blocks.push({ mealLabel: slot.label, recipes }); } }); return blocks; } function recipesForSlot(slotId) { return Object.values(PLANNER_RECIPES).filter((r) => r.allowedSlots.includes(slotId)); } function isCalendarOnToday(mode, weekStart, monthAnchor, selected) { const today = startOfDay(new Date()); if (!sameDay(selected, today)) return false; if (mode === 'week') return weekContains(weekStart, today); return sameMonth(monthAnchor, today); } function syncTodayButton(mode, weekStart, monthAnchor, selected) { const btn = document.getElementById('cal-go-today'); if (!btn) return; const onToday = isCalendarOnToday(mode, weekStart, monthAnchor, selected); const active = 'h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-200 bg-white px-2 text-[10px] font-semibold text-gray-700 shadow-sm hover:bg-gray-50 hover:text-gray-900 transition-colors'; const dim = 'h-6 shrink-0 inline-flex items-center justify-center gap-1 rounded-md border border-gray-100 bg-gray-50 px-2 text-[10px] font-semibold text-gray-400 shadow-none cursor-default transition-colors'; btn.className = onToday ? dim : active; btn.disabled = onToday; } export function getMealPlannerHTML() { return ` `; } function renderWeekGrid(weekStart, selected, plans) { const grid = document.getElementById('calendar-week-grid'); if (!grid) return; const cells = []; for (let i = 0; i < 7; i++) { const day = addDays(weekStart, i); const isSel = selected && sameDay(day, selected); const isToday = sameDay(day, new Date()); const hasMeals = dayHasAnyMeal(plans, day); cells.push(` `); } grid.innerHTML = cells.join(''); } function renderMonthGrid(monthAnchor, selected, plans) { const grid = document.getElementById('calendar-month-grid'); if (!grid) return; const first = startOfMonth(monthAnchor); const startGrid = startOfWeekMonday(first); const cells = []; for (let i = 0; i < 42; i++) { const day = addDays(startGrid, i); const inMonth = day.getMonth() === first.getMonth(); const isSel = selected && sameDay(day, selected); const isToday = sameDay(day, new Date()); const hasMeals = inMonth && dayHasAnyMeal(plans, day); cells.push(` `); } grid.innerHTML = cells.join(''); } function updatePeriodLabel(mode, weekStart, monthAnchor) { const el = document.getElementById('cal-period-label'); if (!el) return; if (mode === 'week') { const end = addDays(weekStart, 6); const y = weekStart.getFullYear(); if (weekStart.getMonth() === end.getMonth()) { el.textContent = `${weekStart.getDate()}–${end.getDate()} ${MONTHS_SHORT[weekStart.getMonth()]} ${y}`; } else { el.textContent = `${weekStart.getDate()} ${MONTHS_SHORT[weekStart.getMonth()]} – ${end.getDate()} ${MONTHS_SHORT[end.getMonth()]} ${y}`; } } else { const m = monthAnchor.getMonth(); const y = monthAnchor.getFullYear(); const monthLong = [ 'Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień', ][m]; el.textContent = `${monthLong} ${y}`; } } function syncModeToggle(mode) { const w = document.getElementById('planner-mode-week'); const m = document.getElementById('planner-mode-month'); const weekWrap = document.getElementById('calendar-week-wrap'); const monthWrap = document.getElementById('calendar-month-wrap'); const base = 'planner-cal-mode-btn w-7 h-6 flex items-center justify-center rounded-[0.3125rem] transition-colors'; const active = `${base} bg-white text-gray-900 shadow-sm`; const idle = `${base} text-gray-400 hover:text-gray-600`; if (w && m) { if (mode === 'week') { w.className = active; m.className = idle; } else { w.className = idle; m.className = active; } } if (weekWrap && monthWrap) { weekWrap.classList.toggle('hidden', mode !== 'week'); monthWrap.classList.toggle('hidden', mode !== 'month'); } } function bindDayClicks(container, state, rerender) { container?.addEventListener('click', (e) => { const btn = e.target.closest('[data-planner-day]'); if (!btn) return; const ts = Number(btn.getAttribute('data-planner-day')); state.selected = new Date(ts); rerender(); }); } 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.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'), 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 heading = document.getElementById('planner-day-heading'); if (heading) { const wd = WEEKDAYS_LONG[sel.getDay()]; heading.textContent = `${wd}, ${sel.getDate()} ${MONTHS_SHORT[sel.getMonth()]}`; } const dayPlan = getDayPlan(state.plans, sel); const totals = sumDayNutrition(dayPlan); const kcalEl = document.getElementById('planner-summary-kcal'); if (kcalEl) { kcalEl.innerHTML = totals.mealCount === 0 ? `— kcal` : `${totals.kcal} kcal`; } const fmt = (n) => `${n} g`; document.getElementById('planner-macro-p').textContent = totals.mealCount ? fmt(totals.protein) : '—'; document.getElementById('planner-macro-f').textContent = totals.mealCount ? fmt(totals.fat) : '—'; document.getElementById('planner-macro-c').textContent = totals.mealCount ? fmt(totals.carbs) : '—'; document.getElementById('planner-detail-kcal').textContent = `${totals.kcal} kcal`; document.getElementById('planner-detail-p').textContent = fmt(totals.protein); document.getElementById('planner-detail-f').textContent = fmt(totals.fat); document.getElementById('planner-detail-c').textContent = fmt(totals.carbs); const ingBtn = document.getElementById('planner-open-ingredients'); if (ingBtn) { ingBtn.disabled = totals.mealCount === 0; ingBtn.classList.toggle('opacity-50', totals.mealCount === 0); ingBtn.classList.toggle('cursor-not-allowed', totals.mealCount === 0); } const slotsRoot = document.getElementById('planner-meal-slots'); if (!slotsRoot) return; slotsRoot.innerHTML = MEAL_SLOTS.map((slot) => { const entries = Array.isArray(dayPlan[slot.id]) ? dayPlan[slot.id] : []; const countLabel = entries.length > 1 ? `${entries.length} dania` : ''; const entryCards = entries.map((entry) => { const recipe = entry && entry.recipeId ? PLANNER_RECIPES[entry.recipeId] : null; if (!recipe) return ''; const servings = Math.max(1, Number(entry.servings) || 1); const n = recipe.nutritionPerServing; const kcal = Math.round(n.kcal * servings); const eid = escapeHtml(entry.id); return `
${escapeHtml(recipe.thumbLabel)}

${escapeHtml(recipe.title)}

${recipe.minutes} min · ${kcal} kcal

Porcje
${servings}
`; }).join(''); const addLabel = entries.length === 0 ? 'Dodaj przepis' : 'Dodaj kolejny przepis'; const addClasses = entries.length === 0 ? 'planner-add-meal w-full py-2.5 rounded-lg border border-dashed border-gray-200 text-sm font-semibold text-gray-700 hover:bg-gray-50 hover:border-gray-300 transition-colors' : 'planner-add-meal w-full py-2 rounded-lg border border-dashed border-gray-200 text-xs font-semibold text-gray-600 hover:bg-gray-50 hover:border-gray-300 transition-colors'; return `
${slot.label} ${countLabel}
${entryCards}
`; }).join(''); } function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderPickerList(slotId) { 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}. Przeciągnij nagłówek w dół lub dotknij tła, by zamknąć.` : ''; const recipes = recipesForSlot(slotId); if (recipes.length === 0) { list.innerHTML = '

Brak dopasowanych przepisów.

'; return; } list.innerHTML = recipes.map((r) => ` `).join(''); } function renderIngredientsSheet(state) { const body = document.getElementById('planner-ing-body'); if (!body) return; const dayPlan = getDayPlan(state.plans, state.selected); const blocks = aggregateDayIngredientsBySlot(dayPlan); if (blocks.length === 0) { body.innerHTML = '

Najpierw zaplanuj posiłki.

'; return; } body.innerHTML = blocks.map((block) => `

${escapeHtml(block.mealLabel)}

${block.recipes.map((rec) => `

${escapeHtml(rec.recipeTitle)}

    ${rec.items.map((ing) => `
  • ${escapeHtml(ing.name)} ${formatAmount(ing.amount)} ${escapeHtml(ing.unit)}
  • `).join('')}
`).join('')}
`).join(''); } 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: 'owsianka', servings: 1 }], obiad: [{ id: newPlanEntryId(), recipeId: 'salatka', servings: 1 }], kolacja: [{ id: newPlanEntryId(), recipeId: 'makaron', 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, nutritionExpanded: false, 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); syncTodayButton(state.mode, state.weekStart, state.monthAnchor, state.selected); if (state.mode === 'week') { renderWeekGrid(state.weekStart, state.selected, state.plans); } else { renderMonthGrid(state.monthAnchor, state.selected, state.plans); } renderDayContent(state); }; const persist = () => { savePlans(state.plans); rerender(); }; bindDayClicks(weekGrid?.parentElement, state, rerender); bindDayClicks(monthGrid?.parentElement, state, rerender); document.getElementById('planner-cal-mode')?.addEventListener('click', (e) => { const btn = e.target.closest('[data-cal-mode]'); if (!btn) return; const mode = btn.getAttribute('data-cal-mode'); if (mode !== 'week' && mode !== 'month') return; state.mode = mode; if (mode === 'week') { state.weekStart = startOfWeekMonday(state.selected); } else { state.monthAnchor = startOfMonth(state.selected); } 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-toggle-nutrition')?.addEventListener('click', () => { state.nutritionExpanded = !state.nutritionExpanded; const details = document.getElementById('planner-nutrition-details'); const chev = document.getElementById('planner-nutrition-chevron'); const btn = document.getElementById('planner-toggle-nutrition'); if (details) details.classList.toggle('hidden', !state.nutritionExpanded); if (chev) chev.classList.toggle('rotate-180', state.nutritionExpanded); if (btn) btn.setAttribute('aria-expanded', state.nutritionExpanded ? 'true' : 'false'); }); document.getElementById('planner-meal-slots')?.addEventListener('click', (e) => { const addBtn = e.target.closest('.planner-add-meal'); if (addBtn) { const slotId = addBtn.getAttribute('data-slot-id'); state.pickerSlot = slotId; renderPickerList(slotId); openSheet(pickerBackdrop, pickerSheet); 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 minus = e.target.closest('.planner-serv-minus'); const plus = e.target.closest('.planner-serv-plus'); const slotId = (minus || plus)?.getAttribute('data-slot-id'); const entryId = (minus || plus)?.getAttribute('data-entry-id'); if (!slotId || !entryId) return; 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; let s = Math.max(1, Number(entry.servings) || 1); if (minus) s = Math.max(1, s - 1); if (plus) s = Math.min(12, s + 1); entry.servings = s; persist(); }); const closePicker = () => { state.pickerSlot = null; closeSheet(pickerBackdrop, pickerSheet); }; 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 || !PLANNER_RECIPES[recipeId]) return; const key = dateKey(state.selected); if (!state.plans[key]) state.plans[key] = {}; const slotId = state.pickerSlot; if (!state.plans[key][slotId]) state.plans[key][slotId] = []; state.plans[key][slotId].push({ id: newPlanEntryId(), recipeId, servings: 1 }); closePicker(); persist(); }); 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 body = document.getElementById('planner-ing-body'); const rows = body?.querySelectorAll('.planner-ing-row'); const n = rows?.length ?? 0; if (n === 0) return; showPlannerToast(`Dodano ${n} składników do listy (zakładka Zakupy w przygotowaniu).`); closeSheet(ingBackdrop, ingSheet); }); document.getElementById('planner-ing-add-btn')?.addEventListener('click', () => { const body = document.getElementById('planner-ing-body'); const selected = body?.querySelectorAll('.planner-ing-row.ingredient-active'); const n = selected?.length ?? 0; if (n === 0) { showPlannerToast('Zaznacz składniki na liście albo użyj „Dodaj wszystkie”.'); return; } showPlannerToast(`Dodano ${n} zaznaczonych pozycji do listy (zakładka Zakupy w przygotowaniu).`); closeSheet(ingBackdrop, ingSheet); }); rerender(); }