Add meal plan editor
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m19s

This commit is contained in:
2026-03-30 22:50:53 +02:00
parent efe1cd941c
commit ee9ddd915e
6 changed files with 829 additions and 405 deletions

View File

@@ -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
? `<span class="text-[10px] font-semibold text-amber-600 tabular-nums shrink-0 ml-auto">${slotKcal} kcal</span>`
@@ -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 ? '<span class="w-1.5 h-1.5 rounded-full bg-amber-400 inline-block shrink-0 ml-1"></span>' : '';
const servLabel = servings > 1 ? `<span class="mx-1.5 text-gray-300">·</span>×${servings}` : '';
return `
<div class="rounded-lg border border-gray-200 bg-white p-2 shadow-sm" data-slot-id="${slot.id}" data-entry-id="${eid}">
<div class="flex items-start justify-between gap-2">
@@ -529,24 +534,21 @@ function renderDayContent(state) {
: `<span class="w-full h-full flex items-center justify-center text-white text-[8px] font-medium">${escapeHtml(recipe.thumbLabel)}</span>`}
</div>
<div class="min-w-0">
<p class="text-[13px] font-bold text-gray-900 truncate underline decoration-1 underline-offset-2">${escapeHtml(recipe.title)}</p>
<div class="flex items-center"><p class="text-[13px] font-bold text-gray-900 truncate underline decoration-1 underline-offset-2">${escapeHtml(recipe.title)}</p>${customDot}</div>
<p class="text-[11px] text-gray-500 mt-0.5 tabular-nums">
<i class="fas fa-clock text-gray-400 mr-0.5" aria-hidden="true"></i>${recipe.minutes} min
<span class="mx-1.5 text-gray-300">·</span>
<i class="fas fa-fire text-gray-400 mr-0.5" aria-hidden="true"></i>${kcal} kcal
<i class="fas fa-fire text-gray-400 mr-0.5" aria-hidden="true"></i>${entryN.kcal} kcal${servLabel}
</p>
</div>
</div>
<button type="button" class="planner-clear-meal w-6 h-6 shrink-0 rounded-full border border-gray-200 text-gray-400 hover:text-red-600 hover:border-red-200 hover:bg-red-50 transition-colors flex items-center justify-center" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Usuń ten przepis">
<i class="fas fa-times text-[9px]" aria-hidden="true"></i>
</button>
</div>
<div class="flex items-center justify-between gap-2 mt-1.5 pt-1.5 border-t border-gray-100">
<span class="text-[11px] font-medium text-gray-500">Porcje</span>
<div class="flex items-center gap-0.5 bg-gray-100 p-0.5 rounded-lg">
<button type="button" class="planner-serv-minus w-6 h-6 bg-white rounded-md shadow-sm flex items-center justify-center text-gray-600 hover:text-black" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Mniej porcji"><i class="fas fa-minus text-[9px]"></i></button>
<span class="planner-serv-count font-bold text-gray-900 text-xs w-5 text-center tabular-nums">${servings}</span>
<button type="button" class="planner-serv-plus w-6 h-6 bg-white rounded-md shadow-sm flex items-center justify-center text-gray-600 hover:text-black" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Więcej porcji"><i class="fas fa-plus text-[9px]"></i></button>
<div class="flex items-center gap-1 shrink-0">
<button type="button" class="planner-edit-meal w-6 h-6 rounded-full border border-gray-200 text-gray-400 hover:text-gray-900 hover:border-gray-400 hover:bg-gray-50 flex items-center justify-center transition-colors" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Edytuj ten przepis">
<i class="fas fa-pencil text-[9px]" aria-hidden="true"></i>
</button>
<button type="button" class="planner-clear-meal w-6 h-6 rounded-full border border-gray-200 text-gray-400 hover:text-red-600 hover:border-red-200 hover:bg-red-50 transition-colors flex items-center justify-center" data-slot-id="${slot.id}" data-entry-id="${eid}" aria-label="Usuń ten przepis">
<i class="fas fa-times text-[9px]" aria-hidden="true"></i>
</button>
</div>
</div>
</div>`;
@@ -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', () => {