From ee9ddd915e7c582fd919bb4612b2b83bd650f46d Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Mon, 30 Mar 2026 22:50:53 +0200 Subject: [PATCH] Add meal plan editor --- js/app.js | 3 + js/services/planIngredients.js | 123 +++++-- js/services/planStore.js | 37 +- js/ui/mealPlanEditor.js | 644 +++++++++++++++++++++++++++++++++ js/views/MealPlanner.js | 80 ++-- js/views/RecipeDetailV2.js | 347 +----------------- 6 files changed, 829 insertions(+), 405 deletions(-) create mode 100644 js/ui/mealPlanEditor.js diff --git a/js/app.js b/js/app.js index f7f23f6..dcfb8c9 100644 --- a/js/app.js +++ b/js/app.js @@ -3,6 +3,7 @@ import { getFilterHTML, setupFilter } from './views/Filter.js?v=2'; import { getRecipeDetailHTML, setupRecipeDetail } from './views/RecipeDetailV2.js?v=2'; import { getMealPlannerHTML, setupMealPlanner } from './views/MealPlanner.js?v=2'; import { getPantryHTML, refreshPantry, setupPantry } from './views/Pantry.js?v=2'; +import { getMealPlanEditorHTML, setupMealPlanEditor } from './ui/mealPlanEditor.js?v=2'; function getAppToastHTML() { return ` @@ -105,6 +106,7 @@ document.addEventListener('DOMContentLoaded', () => { ${getBottomNavHTML()} ${getRecipeDetailHTML()} ${getFilterHTML()} + ${getMealPlanEditorHTML()} ${getAppToastHTML()} `; @@ -114,5 +116,6 @@ document.addEventListener('DOMContentLoaded', () => { setupMealPlanner(); setupPantry(); setupFilter(); + setupMealPlanEditor(); setupRecipeDetail(); }); diff --git a/js/services/planIngredients.js b/js/services/planIngredients.js index dada11e..9a49a1c 100644 --- a/js/services/planIngredients.js +++ b/js/services/planIngredients.js @@ -13,27 +13,79 @@ export function dayHasAnyMeal(plans, d) { }); } +function hasCustomizations(entry) { + return (entry.excludedIngredients?.length > 0) || + (entry.amountOverrides && Object.keys(entry.amountOverrides).length > 0) || + (entry.addedIngredients?.length > 0) || + (entry.substitutions && Object.keys(entry.substitutions).length > 0); +} + +function nutritionForAmountRaw(ingredientId, amount, unit) { + const def = INGREDIENTS[ingredientId]; + if (!def?.nutritionPer100g) return null; + let g = amount; + if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) g = amount * def.weightPerPiece; + const f = g / 100; + return { + kcal: def.nutritionPer100g.kcal * f, + protein: def.nutritionPer100g.protein * f, + fat: def.nutritionPer100g.fat * f, + carbs: def.nutritionPer100g.carbs * f, + }; +} + +export function computeEntryNutrition(entry) { + if (!entry?.recipeId) return { kcal: 0, protein: 0, fat: 0, carbs: 0 }; + const r = RECIPES[entry.recipeId]; + if (!r) return { kcal: 0, protein: 0, fat: 0, carbs: 0 }; + const s = Math.max(1, Number(entry.servings) || 1); + + if (!hasCustomizations(entry)) { + return { + kcal: Math.round(r.nutritionPerServing.kcal * s), + protein: Math.round(r.nutritionPerServing.protein * s * 10) / 10, + fat: Math.round(r.nutritionPerServing.fat * s * 10) / 10, + carbs: Math.round(r.nutritionPerServing.carbs * s * 10) / 10, + }; + } + + const excluded = new Set(entry.excludedIngredients || []); + const overrides = entry.amountOverrides || {}; + const subs = entry.substitutions || {}; + let kcal = 0, protein = 0, fat = 0, carbs = 0; + + for (const ing of r.ingredients) { + if (excluded.has(ing.ingredientId)) continue; + const eid = subs[ing.ingredientId] || ing.ingredientId; + const base = overrides[ing.ingredientId] ?? ing.amount; + const n = nutritionForAmountRaw(eid, base * s, ing.unit); + if (n) { kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; } + } + for (const a of (entry.addedIngredients || [])) { + const n = nutritionForAmountRaw(a.ingredientId, a.amount * s, a.unit); + if (n) { kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; } + } + + return { + kcal: Math.round(kcal), + protein: Math.round(protein * 10) / 10, + fat: Math.round(fat * 10) / 10, + carbs: Math.round(carbs * 10) / 10, + }; +} + export function sumDayNutrition(dayPlan) { - let kcal = 0; - let protein = 0; - let fat = 0; - let carbs = 0; - let mealCount = 0; + let kcal = 0, protein = 0, fat = 0, carbs = 0, mealCount = 0; const skipped = dayPlan._skipped || {}; MEAL_SLOTS.forEach((slot) => { if (skipped[slot.id]) return; const entries = dayPlan[slot.id]; if (!Array.isArray(entries)) return; entries.forEach((entry) => { - if (!entry || !entry.recipeId) return; - const r = RECIPES[entry.recipeId]; - if (!r) return; - const s = Math.max(1, Number(entry.servings) || 1); + if (!entry?.recipeId || !RECIPES[entry.recipeId]) return; mealCount += 1; - kcal += r.nutritionPerServing.kcal * s; - protein += r.nutritionPerServing.protein * s; - fat += r.nutritionPerServing.fat * s; - carbs += r.nutritionPerServing.carbs * s; + const n = computeEntryNutrition(entry); + kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; }); }); return { @@ -70,10 +122,20 @@ export function flattenDayIngredientLines(dayPlan) { const r = RECIPES[entry.recipeId]; if (!r || !Array.isArray(r.ingredients)) return; const serv = Math.max(1, Number(entry.servings) || 1); + const excluded = new Set(entry.excludedIngredients || []); + const overrides = entry.amountOverrides || {}; + const subs = entry.substitutions || {}; r.ingredients.forEach((ing) => { - const scaled = Math.round(ing.amount * serv * 10) / 10; - out.push(resolveLine(ing, scaled)); + if (excluded.has(ing.ingredientId)) return; + const effectiveId = subs[ing.ingredientId] || ing.ingredientId; + const base = overrides[ing.ingredientId] ?? ing.amount; + const scaled = Math.round(base * serv * 10) / 10; + out.push(resolveLine({ ingredientId: effectiveId, unit: ing.unit }, scaled)); }); + for (const a of (entry.addedIngredients || [])) { + const scaled = Math.round(a.amount * serv * 10) / 10; + out.push(resolveLine(a, scaled)); + } }); }); return out; @@ -129,16 +191,33 @@ export function aggregateDayIngredientsBySlot(dayPlan) { const r = RECIPES[entry.recipeId]; if (!r) return; const s = Math.max(1, Number(entry.servings) || 1); - const items = r.ingredients.map((ing) => { - const def = INGREDIENTS[ing.ingredientId]; - return { - ingredientId: ing.ingredientId, - name: def?.name ?? ing.ingredientId, + const excluded = new Set(entry.excludedIngredients || []); + const overrides = entry.amountOverrides || {}; + const subs = entry.substitutions || {}; + const items = []; + r.ingredients.forEach((ing) => { + if (excluded.has(ing.ingredientId)) return; + const effectiveId = subs[ing.ingredientId] || ing.ingredientId; + const base = overrides[ing.ingredientId] ?? ing.amount; + const def = INGREDIENTS[effectiveId]; + items.push({ + ingredientId: effectiveId, + name: def?.name ?? effectiveId, category: def?.category ?? 'inne', - amount: Math.round(ing.amount * s * 10) / 10, + amount: Math.round(base * s * 10) / 10, unit: ing.unit, - }; + }); }); + for (const a of (entry.addedIngredients || [])) { + const def = INGREDIENTS[a.ingredientId]; + items.push({ + ingredientId: a.ingredientId, + name: def?.name ?? a.ingredientId, + category: def?.category ?? 'inne', + amount: Math.round(a.amount * s * 10) / 10, + unit: a.unit, + }); + } recipes.push({ recipeTitle: r.title, items }); }); if (recipes.length > 0) { diff --git a/js/services/planStore.js b/js/services/planStore.js index 4368b3c..6432a39 100644 --- a/js/services/planStore.js +++ b/js/services/planStore.js @@ -1,4 +1,4 @@ -import { RECIPES } from '../data/catalog.js'; +import { INGREDIENTS, RECIPES } from '../data/catalog.js'; import { MEAL_SLOTS } from '../planner/mealSlots.js'; import { PLANS_STORAGE_KEY } from '../storageKeys.js'; import { startOfDay } from './dateUtils.js'; @@ -18,7 +18,32 @@ export function newPlanEntryId() { return `e${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; } -/** Jedna pora dnia = tablica wpisów { id, recipeId, servings } */ +function normalizeEntryExtras(x) { + const out = {}; + if (x.substitutions && typeof x.substitutions === 'object' && !Array.isArray(x.substitutions) && Object.keys(x.substitutions).length > 0) { + out.substitutions = { ...x.substitutions }; + } + if (Array.isArray(x.excludedIngredients)) { + const arr = x.excludedIngredients.filter((id) => typeof id === 'string'); + if (arr.length > 0) out.excludedIngredients = arr; + } + if (x.amountOverrides && typeof x.amountOverrides === 'object' && !Array.isArray(x.amountOverrides)) { + const filtered = {}; + for (const [k, v] of Object.entries(x.amountOverrides)) { + if (typeof v === 'number' && v >= 0) filtered[k] = v; + } + if (Object.keys(filtered).length > 0) out.amountOverrides = filtered; + } + if (Array.isArray(x.addedIngredients)) { + const valid = x.addedIngredients + .filter((a) => a && typeof a.ingredientId === 'string' && INGREDIENTS[a.ingredientId] && typeof a.amount === 'number' && a.amount >= 0 && typeof a.unit === 'string') + .map((a) => ({ ingredientId: a.ingredientId, amount: a.amount, unit: a.unit })); + if (valid.length > 0) out.addedIngredients = valid; + } + return out; +} + +/** Jedna pora dnia = tablica wpisów { id, recipeId, servings, ...extras } */ export function normalizeSlotValue(v) { if (!v) return []; if (Array.isArray(v)) { @@ -28,9 +53,7 @@ export function normalizeSlotValue(v) { 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)), - ...(x.substitutions && typeof x.substitutions === 'object' && !Array.isArray(x.substitutions) && Object.keys(x.substitutions).length > 0 - ? { substitutions: { ...x.substitutions } } - : {}), + ...normalizeEntryExtras(x), })); } if (typeof v === 'object' && v.recipeId && RECIPES[v.recipeId]) { @@ -38,9 +61,7 @@ export function normalizeSlotValue(v) { id: newPlanEntryId(), recipeId: v.recipeId, servings: Math.max(1, Math.min(12, Number(v.servings) || 1)), - ...(v.substitutions && typeof v.substitutions === 'object' && !Array.isArray(v.substitutions) && Object.keys(v.substitutions).length > 0 - ? { substitutions: { ...v.substitutions } } - : {}), + ...normalizeEntryExtras(v), }]; } return []; diff --git a/js/ui/mealPlanEditor.js b/js/ui/mealPlanEditor.js new file mode 100644 index 0000000..57e24c9 --- /dev/null +++ b/js/ui/mealPlanEditor.js @@ -0,0 +1,644 @@ +import { INGREDIENTS, RECIPES } from '../data/catalog.js'; +import { MEAL_SLOTS } from '../planner/mealSlots.js'; +import { + addDays, + addMonths, + sameDay, + sameMonth, + startOfDay, + startOfMonth, + startOfWeekMonday, +} from '../services/dateUtils.js'; +import { + dateKey, + loadPlans, + newPlanEntryId, + savePlans, +} from '../services/planStore.js'; +import { showAppToast } from './toast.js'; + +function esc(s) { + return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +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 WEEKDAYS_SHORT = ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd']; +const WEEKDAYS_LONG = ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota']; +const slotLabel = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label])); + +/* ── HTML template ──────────────────────────────────── */ + +export function getMealPlanEditorHTML() { + return ` + `; +} + +/* ── Setup ──────────────────────────────────────────── */ + +export function setupMealPlanEditor() { + const overlay = document.getElementById('mpe-overlay'); + const sheet = document.getElementById('mpe-sheet'); + if (!overlay || !sheet) return; + + const S = { + mode: null, + recipeId: null, + date: null, + slotId: null, + servings: 1, + subs: {}, + excluded: new Set(), + overrides: {}, + added: [], + entryId: null, + showCal: false, + calDate: null, + calExpanded: false, + altOpen: new Set(), + addOpen: false, + addQuery: '', + }; + + /* ── helpers ───────────────────────────────────── */ + + function nutFor(ingredientId, amount, unit) { + const def = INGREDIENTS[ingredientId]; + if (!def?.nutritionPer100g) return null; + let g = amount; + if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) g = amount * def.weightPerPiece; + const f = g / 100; + return { + kcal: Math.round(def.nutritionPer100g.kcal * f), + protein: Math.round(def.nutritionPer100g.protein * f * 10) / 10, + fat: Math.round(def.nutritionPer100g.fat * f * 10) / 10, + carbs: Math.round(def.nutritionPer100g.carbs * f * 10) / 10, + }; + } + + function totalNutrition() { + const r = RECIPES[S.recipeId]; + if (!r) return { kcal: 0, protein: 0, fat: 0, carbs: 0 }; + let kcal = 0, protein = 0, fat = 0, carbs = 0; + for (const ing of r.ingredients) { + if (S.excluded.has(ing.ingredientId)) continue; + const eid = S.subs[ing.ingredientId] || ing.ingredientId; + const base = S.overrides[ing.ingredientId] ?? ing.amount; + const n = nutFor(eid, base * S.servings, ing.unit); + if (n) { kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; } + } + for (const a of S.added) { + const n = nutFor(a.ingredientId, a.amount * S.servings, a.unit); + if (n) { kcal += n.kcal; protein += n.protein; fat += n.fat; carbs += n.carbs; } + } + return { kcal: Math.round(kcal), protein: Math.round(protein * 10) / 10, fat: Math.round(fat * 10) / 10, carbs: Math.round(carbs * 10) / 10 }; + } + + function fmtAmt(n) { return Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(1))); } + + /* ── Calendar ──────────────────────────────────── */ + + function calCell(d, today, month) { + const sel = S.date && sameDay(d, S.date); + const past = d.getTime() < today.getTime(); + const other = d.getMonth() !== month; + let cls = 'flex flex-col items-center justify-center rounded-md text-xs font-medium transition-colors w-full min-h-10 py-1 gap-0.5 leading-tight '; + if (sel) cls += 'bg-gray-900 text-white '; + else if (past || other) cls += 'text-gray-300 '; + else cls += 'text-gray-800 hover:bg-gray-100 '; + if (!sel && !past && !other && sameDay(d, today)) cls += 'ring-1 ring-inset ring-gray-900 '; + const inner = `${d.getDate()}`; + return (past && !sel) ? `
${inner}
` : ``; + } + + function renderCal() { + const sec = document.getElementById('mpe-cal-section'); + if (!sec || !S.showCal) { sec?.classList.add('hidden'); return; } + sec.classList.remove('hidden'); + const grid = document.getElementById('mpe-cal-grid'); + const title = document.getElementById('mpe-cal-title'); + const todayBtn = document.getElementById('mpe-cal-today'); + const icon = document.getElementById('mpe-cal-toggle-icon'); + if (!grid || !title) return; + const today = startOfDay(new Date()); + + if (S.calExpanded) { + const ms = startOfMonth(S.calDate); + title.textContent = `${MONTHS_LONG[ms.getMonth()]} ${ms.getFullYear()}`; + icon.className = 'fas fa-chevron-up text-[10px]'; + const first = startOfWeekMonday(ms); + const cells = []; + let d = new Date(first); + for (let i = 0; i < 42; i++) { cells.push(new Date(d)); d = addDays(d, 1); } + while (cells.length > 7 && cells.slice(-7).every((c) => c.getMonth() !== ms.getMonth())) cells.splice(-7); + grid.innerHTML = cells.map((c) => calCell(c, today, ms.getMonth())).join(''); + todayBtn?.classList.toggle('hidden', sameMonth(today, S.calDate)); + } else { + const ws = startOfWeekMonday(S.calDate); + title.textContent = `${MONTHS_LONG[S.calDate.getMonth()]} ${S.calDate.getFullYear()}`; + icon.className = 'fas fa-chevron-down text-[10px]'; + const cells = []; + for (let i = 0; i < 7; i++) cells.push(addDays(ws, i)); + grid.innerHTML = cells.map((c) => calCell(c, today, S.calDate.getMonth())).join(''); + todayBtn?.classList.toggle('hidden', sameDay(startOfWeekMonday(today), ws)); + } + + grid.querySelectorAll('.mpe-cal-day').forEach((btn) => { + btn.addEventListener('click', () => { + S.date = new Date(Number(btn.dataset.ts)); + S.calDate = new Date(S.date); + renderCal(); + }); + }); + } + + function renderSlots() { + const el = document.getElementById('mpe-slot-chips'); + if (!el || !S.showCal) return; + const r = RECIPES[S.recipeId]; + if (!r) return; + el.innerHTML = MEAL_SLOTS.filter((s) => r.allowedSlots.includes(s.id)).map((s) => { + const sel = s.id === S.slotId; + return ``; + }).join(''); + } + + /* ── Servings ──────────────────────────────────── */ + + function renderServings() { + const el = document.getElementById('mpe-servings-row'); + if (!el) return; + el.innerHTML = ` + Porcje +
+ + ${S.servings} + +
`; + } + + /* ── Ingredients list ──────────────────────────── */ + + function renderIngList() { + const list = document.getElementById('mpe-ing-list'); + if (!list) return; + const r = RECIPES[S.recipeId]; + if (!r) return; + let html = ''; + + for (const ing of r.ingredients) { + const id = ing.ingredientId; + const excl = S.excluded.has(id); + const eid = S.subs[id] || id; + const eDef = INGREDIENTS[eid]; + const eName = eDef?.name || eid; + const hasAlts = ing.alternatives?.length > 0; + const swapped = eid !== id; + const altOpen = S.altOpen.has(id); + const base = S.overrides[id] ?? ing.amount; + const disp = base * S.servings; + const modified = id in S.overrides; + + const checkCls = excl + ? 'w-5 h-5 rounded-md border-2 border-gray-300 bg-white' + : 'w-5 h-5 rounded-md border-2 border-gray-900 bg-gray-900'; + const checkIco = excl ? '' : ''; + const rowBorder = excl ? 'border-gray-100' : swapped ? 'border-amber-200' : 'border-gray-200'; + const rowBg = excl ? 'bg-gray-50/50' : swapped ? 'bg-amber-50/30' : 'bg-white'; + const rowOp = excl ? 'opacity-50' : ''; + const nameCls = excl ? 'text-[12px] font-semibold text-gray-400 line-through' : 'text-[12px] font-semibold text-gray-900'; + const amtCls = excl ? 'text-gray-300' : 'text-gray-900'; + const unitCls = excl ? 'text-gray-300' : 'text-gray-500'; + + const shuffleBtn = hasAlts && !excl + ? `` + : ''; + const modDot = modified && !excl ? '' : ''; + + html += `
`; + html += `
`; + html += ``; + html += `
${esc(eName)}${shuffleBtn}
`; + html += ``; + html += `
`; + + if (hasAlts && altOpen && !excl) { + const opts = [id, ...ing.alternatives]; + html += '
'; + for (const altId of opts) { + const def = INGREDIENTS[altId]; + const name = def?.name || altId; + const isSel = eid === altId; + const isOrig = altId === id; + const cls2 = isSel ? 'border-gray-900 bg-gray-50 ring-1 ring-gray-900' : 'border-gray-200 bg-white hover:border-gray-300'; + const radio = `
${isSel ? '
' : ''}
`; + const tag = isOrig ? `Domyślny` : ''; + const n = nutFor(altId, disp, ing.unit); + const nLine = n ? `
${n.kcal} kcal · ${n.protein}g B · ${n.fat}g T · ${n.carbs}g W
` : ''; + html += ``; + } + html += '
'; + } + + html += '
'; + } + + for (const a of S.added) { + const def = INGREDIENTS[a.ingredientId]; + const name = def?.name || a.ingredientId; + const disp = a.amount * S.servings; + html += `
+
+ + ${esc(name)} + + +
+
`; + } + + list.innerHTML = html; + } + + function renderAddArea() { + const el = document.getElementById('mpe-add-area'); + if (!el) return; + if (!S.addOpen) { + el.innerHTML = ``; + return; + } + const recipe = RECIPES[S.recipeId]; + const usedIds = new Set([ + ...(recipe?.ingredients.map((i) => i.ingredientId) || []), + ...S.added.map((a) => a.ingredientId), + ]); + const q = S.addQuery.toLowerCase().trim(); + const avail = Object.values(INGREDIENTS).filter((i) => !usedIds.has(i.id) && (!q || i.name.toLowerCase().includes(q))); + el.innerHTML = ` +
+
+ + +
+
+ ${avail.length === 0 + ? '

Brak wyników

' + : avail.slice(0, 20).map((i) => ``).join('')} +
+
`; + } + + function updateAddResults() { + const results = document.getElementById('mpe-add-results'); + if (!results) return; + const recipe = RECIPES[S.recipeId]; + const usedIds = new Set([ + ...(recipe?.ingredients.map((i) => i.ingredientId) || []), + ...S.added.map((a) => a.ingredientId), + ]); + const q = S.addQuery.toLowerCase().trim(); + const avail = Object.values(INGREDIENTS).filter((i) => !usedIds.has(i.id) && (!q || i.name.toLowerCase().includes(q))); + results.innerHTML = avail.length === 0 + ? '

Brak wyników

' + : avail.slice(0, 20).map((i) => ``).join(''); + } + + function renderIngredients() { + renderIngList(); + renderAddArea(); + } + + /* ── Nutrition summary ─────────────────────────── */ + + function renderNutrition() { + const el = document.getElementById('mpe-nutrition-section'); + if (!el) return; + const n = totalNutrition(); + el.innerHTML = ` +
+

Wartości odżywcze${S.servings > 1 ? ` · ${S.servings} porcje` : ''}

+
+
${n.kcal}kcal
+
${n.protein} gBiałko
+
${n.carbs} gWęgle
+
${n.fat} gTłuszcze
+
+
`; + } + + /* ── Render all ────────────────────────────────── */ + + function renderAll() { renderCal(); renderSlots(); renderServings(); renderIngredients(); renderNutrition(); } + + /* ── Open / Close ─────────────────────────────── */ + + function openEditor(opts) { + const recipe = RECIPES[opts.recipeId]; + if (!recipe) return; + + S.mode = opts.mode || 'add'; + S.recipeId = opts.recipeId; + S.servings = opts.servings || opts.entry?.servings || 1; + S.subs = { ...(opts.substitutions || opts.entry?.substitutions || {}) }; + S.excluded = new Set(opts.entry?.excludedIngredients || []); + S.overrides = { ...(opts.entry?.amountOverrides || {}) }; + S.added = (opts.entry?.addedIngredients || []).map((a) => ({ ...a })); + S.entryId = opts.entry?.id || null; + S.altOpen = new Set(); + S.addOpen = false; + S.addQuery = ''; + + if (opts.date && opts.slotId) { + S.date = opts.date; + S.slotId = opts.slotId; + S.showCal = false; + } else { + const today = startOfDay(new Date()); + S.date = today; + S.calDate = today; + S.calExpanded = false; + S.slotId = recipe.allowedSlots[0] || MEAL_SLOTS[0]?.id; + S.showCal = true; + } + + document.getElementById('mpe-title').textContent = S.mode === 'edit' ? 'Edytuj posiłek' : 'Zaplanuj posiłek'; + document.getElementById('mpe-subtitle').textContent = recipe.title; + document.getElementById('mpe-confirm-label').textContent = S.mode === 'edit' ? 'Zapisz zmiany' : 'Dodaj do planu'; + + renderAll(); + + overlay.classList.remove('hidden'); + overlay.style.pointerEvents = 'auto'; + requestAnimationFrame(() => { sheet.style.transform = 'translateY(0)'; }); + } + + function closeEditor() { + sheet.style.transform = 'translateY(100%)'; + setTimeout(() => { overlay.classList.add('hidden'); overlay.style.pointerEvents = 'none'; }, 300); + } + + /* ── Save ──────────────────────────────────────── */ + + function buildEntry() { + const entry = { id: S.entryId || newPlanEntryId(), recipeId: S.recipeId, servings: S.servings }; + if (Object.keys(S.subs).length > 0) entry.substitutions = { ...S.subs }; + if (S.excluded.size > 0) entry.excludedIngredients = [...S.excluded]; + const recipe = RECIPES[S.recipeId]; + if (recipe) { + const ov = {}; + for (const [k, v] of Object.entries(S.overrides)) { + const orig = recipe.ingredients.find((i) => i.ingredientId === k); + if (orig && Math.abs(v - orig.amount) > 0.01) ov[k] = v; + } + if (Object.keys(ov).length > 0) entry.amountOverrides = ov; + } + if (S.added.length > 0) entry.addedIngredients = S.added.map((a) => ({ ingredientId: a.ingredientId, amount: a.amount, unit: a.unit })); + return entry; + } + + function handleConfirm() { + if (!S.recipeId || !S.date || !S.slotId) return; + const plans = loadPlans(); + const key = dateKey(S.date); + + if (S.mode === 'edit' && S.entryId) { + const arr = plans[key]?.[S.slotId]; + if (Array.isArray(arr)) { + const idx = arr.findIndex((e) => e.id === S.entryId); + if (idx >= 0) arr[idx] = buildEntry(); + } + } else { + if (!plans[key]) plans[key] = {}; + if (!plans[key][S.slotId]) plans[key][S.slotId] = []; + plans[key][S.slotId].push(buildEntry()); + } + + savePlans(plans); + closeEditor(); + showAppToast(S.mode === 'edit' ? 'Zapisano zmiany!' : 'Dodano do planera!'); + window.refreshPlanner?.(); + } + + /* ── Event bindings ───────────────────────────── */ + + document.getElementById('mpe-close-btn')?.addEventListener('click', closeEditor); + overlay?.addEventListener('click', (e) => { if (e.target === overlay) closeEditor(); }); + document.getElementById('mpe-confirm-btn')?.addEventListener('click', handleConfirm); + + document.getElementById('mpe-cal-prev')?.addEventListener('click', () => { + if (!S.showCal) return; + S.calDate = S.calExpanded ? addMonths(S.calDate, -1) : addDays(S.calDate, -7); + renderCal(); + }); + document.getElementById('mpe-cal-next')?.addEventListener('click', () => { + if (!S.showCal) return; + S.calDate = S.calExpanded ? addMonths(S.calDate, 1) : addDays(S.calDate, 7); + renderCal(); + }); + document.getElementById('mpe-cal-today')?.addEventListener('click', () => { + const today = startOfDay(new Date()); + S.date = today; S.calDate = today; + renderCal(); + }); + document.getElementById('mpe-cal-toggle')?.addEventListener('click', () => { + S.calExpanded = !S.calExpanded; + renderCal(); + }); + + document.getElementById('mpe-slot-chips')?.addEventListener('click', (e) => { + const btn = e.target.closest('.mpe-slot-btn'); + if (!btn) return; + S.slotId = btn.dataset.slotId; + renderSlots(); + }); + + document.getElementById('mpe-servings-row')?.addEventListener('click', (e) => { + if (e.target.closest('#mpe-serv-minus')) { if (S.servings <= 1) return; S.servings--; } + else if (e.target.closest('#mpe-serv-plus')) { if (S.servings >= 12) return; S.servings++; } + else return; + renderServings(); + renderIngredients(); + renderNutrition(); + }); + + /* ── Ingredient section delegation ────────────── */ + + const ingSec = document.getElementById('mpe-ing-section'); + + ingSec?.addEventListener('click', (e) => { + const toggle = e.target.closest('.mpe-toggle'); + if (toggle) { + const id = toggle.dataset.origId; + if (S.excluded.has(id)) S.excluded.delete(id); else S.excluded.add(id); + renderIngredients(); renderNutrition(); + return; + } + + const shuffle = e.target.closest('.mpe-shuffle'); + if (shuffle) { + const id = shuffle.dataset.origId; + if (S.altOpen.has(id)) S.altOpen.delete(id); else S.altOpen.add(id); + renderIngList(); + return; + } + + const altPick = e.target.closest('.mpe-alt-pick'); + if (altPick) { + const origId = altPick.dataset.origId; + const altId = altPick.dataset.altId; + if (altId === origId) delete S.subs[origId]; else S.subs[origId] = altId; + S.altOpen.delete(origId); + renderIngList(); renderNutrition(); + return; + } + + const editAmt = e.target.closest('.mpe-edit-amt'); + if (editAmt && !editAmt.disabled) { + startAmountEdit(editAmt); + return; + } + + const removeAdded = e.target.closest('.mpe-remove-added'); + if (removeAdded) { + S.added = S.added.filter((a) => a.ingredientId !== removeAdded.dataset.ingId); + renderIngredients(); renderNutrition(); + return; + } + + if (e.target.closest('#mpe-add-btn')) { + S.addOpen = true; S.addQuery = ''; + renderAddArea(); + document.getElementById('mpe-add-search')?.focus(); + return; + } + + if (e.target.closest('#mpe-add-cancel')) { + S.addOpen = false; + renderAddArea(); + return; + } + + const addPick = e.target.closest('.mpe-add-pick'); + if (addPick) { + const ingId = addPick.dataset.ingId; + const def = INGREDIENTS[ingId]; + if (!def) return; + const unit = def.pantryUnit === 'szt' ? 'szt.' : def.pantryUnit; + const amt = def.pantryUnit === 'szt' ? 1 : 100; + S.added.push({ ingredientId: ingId, amount: amt, unit }); + S.addOpen = false; + renderIngredients(); renderNutrition(); + } + }); + + ingSec?.addEventListener('input', (e) => { + if (e.target.id === 'mpe-add-search') { + S.addQuery = e.target.value; + updateAddResults(); + } + }); + + /* ── Inline amount editing ────────────────────── */ + + function startAmountEdit(btn) { + const type = btn.dataset.type; + const id = btn.dataset.origId || btn.dataset.ingId; + const amtSpan = btn.querySelector('.tabular-nums'); + if (!amtSpan) return; + + const currentDisplay = parseFloat(amtSpan.textContent); + const spans = btn.querySelectorAll('span'); + const unitText = spans.length > 0 ? spans[spans.length - 1].textContent.trim() : ''; + const saved = btn.innerHTML; + + btn.innerHTML = `${esc(unitText)}`; + + const input = btn.querySelector('.mpe-amt-input'); + if (!input) { btn.innerHTML = saved; return; } + input.focus(); + input.select(); + + let done = false; + const finish = (save) => { + if (done) return; + done = true; + if (save) { + const raw = Math.max(0, parseFloat(input.value) || 0); + const newBase = S.servings > 0 ? raw / S.servings : raw; + const rounded = Math.round(newBase * 100) / 100; + + if (type === 'recipe') { + const recipe = RECIPES[S.recipeId]; + const orig = recipe?.ingredients.find((i) => i.ingredientId === id); + if (orig && Math.abs(rounded - orig.amount) > 0.01) S.overrides[id] = rounded; + else delete S.overrides[id]; + } else if (type === 'added') { + const a = S.added.find((x) => x.ingredientId === id); + if (a) a.amount = rounded; + } + } + renderIngList(); + renderNutrition(); + }; + + input.addEventListener('blur', () => finish(true)); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); input.blur(); } + if (e.key === 'Escape') { e.preventDefault(); finish(false); } + }); + } + + /* ── Expose globally ──────────────────────────── */ + + window.openMealPlanEditor = openEditor; + window.closeMealPlanEditor = closeEditor; +} diff --git a/js/views/MealPlanner.js b/js/views/MealPlanner.js index db188e3..d489814 100644 --- a/js/views/MealPlanner.js +++ b/js/views/MealPlanner.js @@ -12,6 +12,7 @@ import { weekContains, } from '../services/dateUtils.js'; import { + computeEntryNutrition, computeFullForecast, countDayShortfalls, dayHasAnyMeal, @@ -502,8 +503,7 @@ function renderDayContent(state) { let slotKcal = 0; entries.forEach((entry) => { - const r = entry?.recipeId ? RECIPES[entry.recipeId] : null; - if (r) slotKcal += Math.round(r.nutritionPerServing.kcal * Math.max(1, Number(entry.servings) || 1)); + if (entry?.recipeId && RECIPES[entry.recipeId]) slotKcal += computeEntryNutrition(entry).kcal; }); const kcalBadge = slotKcal > 0 ? `${slotKcal} kcal` @@ -516,9 +516,14 @@ function renderDayContent(state) { const recipe = entry && entry.recipeId ? 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 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 `
@@ -529,24 +534,21 @@ function renderDayContent(state) { : `${escapeHtml(recipe.thumbLabel)}`}
-

${escapeHtml(recipe.title)}

+

${escapeHtml(recipe.title)}

${customDot}

${recipe.minutes} min · - ${kcal} kcal + ${entryN.kcal} kcal${servLabel}

- - -
- Porcje -
- - ${servings} - +
+ +
`; @@ -1016,6 +1018,24 @@ export function setupMealPlanner() { 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'); @@ -1031,21 +1051,6 @@ export function setupMealPlanner() { 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 = () => { @@ -1069,13 +1074,16 @@ export function setupMealPlanner() { if (!pick || !state.pickerSlot) return; const recipeId = pick.getAttribute('data-recipe-id'); if (!recipeId || !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(); + setTimeout(() => { + window.openMealPlanEditor?.({ + mode: 'add', + recipeId, + date: state.selected, + slotId, + }); + }, 320); }); document.getElementById('planner-open-ingredients')?.addEventListener('click', () => { diff --git a/js/views/RecipeDetailV2.js b/js/views/RecipeDetailV2.js index 8900827..97a0ce0 100644 --- a/js/views/RecipeDetailV2.js +++ b/js/views/RecipeDetailV2.js @@ -1,8 +1,5 @@ import { RECIPES, INGREDIENTS } from '../data/catalog.js'; import { MEAL_SLOTS } from '../planner/mealSlots.js'; -import { addDays, addMonths, sameDay, sameMonth, startOfDay, startOfMonth, startOfWeekMonday } from '../services/dateUtils.js'; -import { dateKey, loadPlans, newPlanEntryId, savePlans } from '../services/planStore.js'; -import { showAppToast } from '../ui/toast.js'; function escapeHtml(s) { return String(s) @@ -13,8 +10,6 @@ function escapeHtml(s) { } const slotLabelMap = Object.fromEntries(MEAL_SLOTS.map((s) => [s.id, s.label])); -const MONTHS_LONG = ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień']; -const WEEKDAYS_SHORT = ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd']; export function getRecipeDetailHTML() { return ` @@ -28,45 +23,6 @@ export function getRecipeDetailHTML() { - -
@@ -199,11 +155,6 @@ function nutritionForAmount(ingredientId, amount, unit) { }; } -function renderNutritionLine(nutrition) { - if (!nutrition) return ''; - return `
${nutrition.kcal} kcal${nutrition.protein}g białko${nutrition.fat}g tłuszcz${nutrition.carbs}g węgle
`; -} - /* ── ingredients tab with inline nutrition + summary ───── */ function computeEffectiveNutritionTotals(recipe) { @@ -443,298 +394,16 @@ export function setupRecipeDetail() { } }); - /* ── planner picker ──────────────────────────────── */ + /* ── planner — delegate to MealPlanEditor ─────── */ - let plannerPickerDay = null; - let plannerPickerSlot = null; - let calViewDate = null; - let calExpanded = false; - - const plannerOverlay = document.getElementById('rd-planner-picker'); - const plannerSheet = document.getElementById('rd-planner-sheet'); - - function renderCalendarCell(d, today, activeMonth) { - const isToday = sameDay(d, today); - const isSelected = plannerPickerDay && sameDay(d, plannerPickerDay); - const isOtherMonth = d.getMonth() !== activeMonth; - const isPast = d.getTime() < today.getTime(); - - let cls = 'flex flex-col items-center justify-center rounded-md text-xs font-medium transition-colors w-full min-h-10 py-1 gap-0.5 leading-tight '; - - if (isSelected) { - cls += 'bg-gray-900 text-white '; - } else if (isPast) { - cls += 'text-gray-300 '; - } else if (isOtherMonth) { - cls += 'text-gray-300 '; - } else { - cls += 'text-gray-800 hover:bg-gray-100 '; - } - - if (isToday && !isSelected && !isPast) { - cls += 'ring-1 ring-inset ring-gray-900 '; - } - - const inner = `${d.getDate()}`; - - if (isPast && !isSelected) { - return `
${inner}
`; - } - - return ``; - } - - function renderPlannerCalendar() { - const grid = document.getElementById('rd-cal-grid'); - const titleEl = document.getElementById('rd-cal-title'); - const todayBtn = document.getElementById('rd-cal-today'); - const toggleIcon = document.getElementById('rd-cal-toggle-icon'); - if (!grid || !titleEl) return; - - const today = startOfDay(new Date()); - - if (calExpanded) { - const ms = startOfMonth(calViewDate); - titleEl.textContent = `${MONTHS_LONG[ms.getMonth()]} ${ms.getFullYear()}`; - toggleIcon.className = 'fas fa-chevron-up text-[10px]'; - - const firstCell = startOfWeekMonday(ms); - const cells = []; - let d = new Date(firstCell); - for (let i = 0; i < 42; i++) { - cells.push(new Date(d)); - d = addDays(d, 1); - } - while (cells.length > 7) { - const last7 = cells.slice(-7); - if (last7.every((c) => c.getMonth() !== ms.getMonth())) cells.splice(-7); - else break; - } - - grid.innerHTML = cells.map((c) => renderCalendarCell(c, today, ms.getMonth())).join(''); - todayBtn.classList.toggle('hidden', sameMonth(today, calViewDate)); - } else { - const ws = startOfWeekMonday(calViewDate); - titleEl.textContent = `${MONTHS_LONG[calViewDate.getMonth()]} ${calViewDate.getFullYear()}`; - toggleIcon.className = 'fas fa-chevron-down text-[10px]'; - - const cells = []; - for (let i = 0; i < 7; i++) cells.push(addDays(ws, i)); - grid.innerHTML = cells.map((c) => renderCalendarCell(c, today, calViewDate.getMonth())).join(''); - - const todayWs = startOfWeekMonday(today); - todayBtn.classList.toggle('hidden', sameDay(ws, todayWs)); - } - - grid.querySelectorAll('.rd-cal-day').forEach((btn) => { - btn.addEventListener('click', () => { - plannerPickerDay = new Date(Number(btn.dataset.ts)); - calViewDate = new Date(plannerPickerDay); - renderPlannerCalendar(); - }); - }); - } - - document.getElementById('rd-cal-prev')?.addEventListener('click', () => { - calViewDate = calExpanded ? addMonths(calViewDate, -1) : addDays(calViewDate, -7); - renderPlannerCalendar(); - }); - document.getElementById('rd-cal-next')?.addEventListener('click', () => { - calViewDate = calExpanded ? addMonths(calViewDate, 1) : addDays(calViewDate, 7); - renderPlannerCalendar(); - }); - document.getElementById('rd-cal-today')?.addEventListener('click', () => { - const today = startOfDay(new Date()); - plannerPickerDay = today; - calViewDate = today; - renderPlannerCalendar(); - }); - document.getElementById('rd-cal-toggle')?.addEventListener('click', () => { - calExpanded = !calExpanded; - renderPlannerCalendar(); - }); - - /* ── planner variant selection ────────────────────── */ - - const expandedVariantGroups = new Set(); - - function renderPlannerVariants(recipe) { - const section = document.getElementById('rd-planner-variant'); - const container = document.getElementById('rd-planner-variant-options'); - if (!section || !container) return; - - const altsIngredients = recipe.ingredients.filter((i) => i.alternatives?.length > 0); - if (altsIngredients.length === 0) { - section.classList.add('hidden'); - return; - } - section.classList.remove('hidden'); - - let html = ''; - for (const ing of altsIngredients) { - const origId = ing.ingredientId; - const scaledAmount = ing.amount * currentServings; - const displayAmount = Number.isInteger(scaledAmount) ? scaledAmount : parseFloat(scaledAmount.toFixed(1)); - const effectiveId = getEffectiveIngredientId(origId); - const effectiveDef = INGREDIENTS[effectiveId]; - const effectiveName = effectiveDef?.name || effectiveId; - const isExpanded = expandedVariantGroups.has(origId); - const isSwapped = effectiveId !== origId; - - html += `
- `; - - if (isExpanded) { - const allOptions = [origId, ...ing.alternatives]; - html += '
'; - for (const altId of allOptions) { - const def = INGREDIENTS[altId]; - const altName = def?.name || altId; - const isSelected = effectiveId === altId; - const isOriginal = altId === origId; - const nutrition = nutritionForAmount(altId, scaledAmount, ing.unit); - - const selectedCls = isSelected - ? 'border-gray-900 bg-gray-50 ring-1 ring-gray-900' - : 'border-gray-200 bg-white hover:border-gray-300'; - - let tag = ''; - if (isOriginal) tag = `Domyślny`; - - html += ` - `; - } - html += '
'; - } - - html += '
'; - } - - container.innerHTML = html; - } - - document.getElementById('rd-planner-variant-options')?.addEventListener('click', (e) => { - const toggle = e.target.closest('.rd-variant-toggle'); - if (toggle) { - const origId = toggle.dataset.originalId; - if (expandedVariantGroups.has(origId)) expandedVariantGroups.delete(origId); - else expandedVariantGroups.add(origId); - const recipe = RECIPES[currentRecipeId]; - if (recipe) renderPlannerVariants(recipe); - return; - } - - const btn = e.target.closest('.rd-variant-option'); - if (!btn) return; - const originalId = btn.dataset.originalId; - const altId = btn.dataset.altId; - if (altId === originalId) { - delete currentSubstitutions[originalId]; - } else { - currentSubstitutions[originalId] = altId; - } - expandedVariantGroups.delete(originalId); - const recipe = RECIPES[currentRecipeId]; - if (recipe) renderPlannerVariants(recipe); - }); - - function openPlannerPicker() { - const recipe = RECIPES[currentRecipeId]; - if (!recipe) return; - - document.getElementById('rd-planner-recipe-name').textContent = recipe.title; - expandedVariantGroups.clear(); - - const today = startOfDay(new Date()); - plannerPickerDay = today; - calViewDate = today; - calExpanded = false; - renderPlannerCalendar(); - - renderPlannerVariants(recipe); - - plannerPickerSlot = recipe.allowedSlots[0] || MEAL_SLOTS[0]?.id; - const slotsContainer = document.getElementById('rd-planner-slots'); - slotsContainer.innerHTML = MEAL_SLOTS.filter((s) => recipe.allowedSlots.includes(s.id)).map((s) => { - const sel = s.id === plannerPickerSlot; - return ``; - }).join(''); - - plannerOverlay.classList.remove('hidden'); - plannerOverlay.style.pointerEvents = 'auto'; - requestAnimationFrame(() => { - plannerSheet.style.transform = 'translateY(0)'; - }); - } - - function closePlannerPicker() { - plannerSheet.style.transform = 'translateY(100%)'; - setTimeout(() => { - plannerOverlay.classList.add('hidden'); - plannerOverlay.style.pointerEvents = 'none'; - }, 300); - } - - document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', openPlannerPicker); - - plannerOverlay?.addEventListener('click', (e) => { - if (e.target === plannerOverlay) closePlannerPicker(); - }); - - document.getElementById('rd-planner-slots')?.addEventListener('click', (e) => { - const btn = e.target.closest('.rd-plan-slot-btn'); - if (!btn) return; - plannerPickerSlot = btn.getAttribute('data-slot-id'); - document.querySelectorAll('.rd-plan-slot-btn').forEach((b) => { - b.classList.remove('border-gray-900', 'bg-gray-900', 'text-white'); - b.classList.add('border-gray-200', 'bg-gray-50', 'text-gray-700'); - }); - btn.classList.remove('border-gray-200', 'bg-gray-50', 'text-gray-700'); - btn.classList.add('border-gray-900', 'bg-gray-900', 'text-white'); - }); - - document.getElementById('rd-planner-confirm')?.addEventListener('click', () => { - if (!currentRecipeId || !plannerPickerDay || !plannerPickerSlot) return; - const plans = loadPlans(); - const key = dateKey(plannerPickerDay); - if (!plans[key]) plans[key] = {}; - if (!plans[key][plannerPickerSlot]) plans[key][plannerPickerSlot] = []; - - const entry = { - id: newPlanEntryId(), + document.getElementById('rd-add-to-planner-btn')?.addEventListener('click', () => { + if (!currentRecipeId) return; + window.openMealPlanEditor?.({ + mode: 'add', recipeId: currentRecipeId, servings: currentServings, - }; - if (Object.keys(currentSubstitutions).length > 0) { - entry.substitutions = { ...currentSubstitutions }; - } - - plans[key][plannerPickerSlot].push(entry); - savePlans(plans); - closePlannerPicker(); - showAppToast('Dodano do planera!'); - window.refreshPlanner?.(); + substitutions: { ...currentSubstitutions }, + }); }); window.openRecipeDetail = (recipeId) => { @@ -746,7 +415,7 @@ export function setupRecipeDetail() { }; window.closeRecipeDetail = () => { - closePlannerPicker(); + window.closeMealPlanEditor?.(); const view = document.getElementById('recipe-detail-view'); view.classList.remove('translate-x-0', 'opacity-100', 'pointer-events-auto'); view.classList.add('translate-x-full', 'opacity-0', 'pointer-events-none');