Files
homelab/stacks/recipe/js/services/planIngredients.js

142 lines
4.8 KiB
JavaScript

import { INGREDIENTS, RECIPES } from '../data/catalog.js';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
import { addDays } from './dateUtils.js';
import { getDayPlan } from './planStore.js';
export 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;
});
}
export 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 = 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,
};
}
function resolveLine(ing, scaledAmount) {
const def = INGREDIENTS[ing.ingredientId];
return {
ingredientId: ing.ingredientId,
name: def?.name ?? ing.ingredientId,
category: def?.category ?? 'inne',
amount: scaledAmount,
unit: ing.unit,
};
}
/** Płaska lista składników z jednego dnia (wszystkie pory). */
export function flattenDayIngredientLines(dayPlan) {
/** @type {ReturnType<typeof resolveLine>[]} */
const out = [];
MEAL_SLOTS.forEach((slot) => {
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 || !Array.isArray(r.ingredients)) return;
const serv = Math.max(1, Number(entry.servings) || 1);
r.ingredients.forEach((ing) => {
const scaled = Math.round(ing.amount * serv * 10) / 10;
out.push(resolveLine(ing, scaled));
});
});
});
return out;
}
/** Sumuje po (ingredientId + unit) — ta sama jednostka jak w przepisie. */
export function mergeIngredientLines(lines) {
const m = new Map();
for (const L of lines) {
const key = `${L.ingredientId}\t${L.unit}`;
const cur = m.get(key);
if (!cur) {
m.set(key, { ...L });
} else {
cur.amount = Math.round((cur.amount + L.amount) * 10) / 10;
}
}
return [...m.values()].sort((a, b) => {
const c = a.category.localeCompare(b.category);
return c !== 0 ? c : a.name.localeCompare(b.name);
});
}
/**
* Zapotrzebowanie składników od weekStart (włącznie) przez 7 dni.
* @param {Record<string, unknown>} plans
* @param {Date} weekStart
*/
export function aggregateWeekIngredientNeed(plans, weekStart) {
const all = [];
for (let i = 0; i < 7; i++) {
const day = addDays(weekStart, i);
const dayPlan = getDayPlan(plans, day);
all.push(...flattenDayIngredientLines(dayPlan));
}
return mergeIngredientLines(all);
}
/**
* Jedna grupa na porę dnia: nagłówek pory raz, potem bloki przepisów ze składnikami.
*/
export function aggregateDayIngredientsBySlot(dayPlan) {
/** @type {{ mealLabel: string, recipes: { recipeTitle: string, items: { ingredientId: string, name: string, amount: number, unit: string, category: 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 = 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,
category: def?.category ?? 'inne',
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;
}