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

@@ -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) {

View File

@@ -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 [];