192 lines
6.6 KiB
JavaScript
192 lines
6.6 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;
|
|
}
|
|
|
|
export function countDayShortfalls(dayPlan, pantry) {
|
|
const lines = mergeIngredientLines(flattenDayIngredientLines(dayPlan));
|
|
let count = 0;
|
|
for (const line of lines) {
|
|
if ((Number(pantry[line.ingredientId]) || 0) < line.amount) count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Kumulatywna prognoza zużycia spiżarni: od startDate przez lookAheadDays dni.
|
|
* Zwraca tablicę dni (tylko te z posiłkami), każdy z listą składników i informacją
|
|
* ile jest w spiżarni (po odjęciu zużycia z poprzednich dni) i ile brakuje.
|
|
*/
|
|
export function computeFullForecast(plans, pantry, startDate, lookAheadDays = 8) {
|
|
const running = { ...pantry };
|
|
const days = [];
|
|
|
|
for (let i = 0; i < lookAheadDays; i++) {
|
|
const day = addDays(startDate, i);
|
|
const dayPlan = getDayPlan(plans, day);
|
|
const lines = mergeIngredientLines(flattenDayIngredientLines(dayPlan));
|
|
if (lines.length === 0) continue;
|
|
|
|
const items = lines.map((line) => {
|
|
const def = INGREDIENTS[line.ingredientId];
|
|
const have = Math.round((Number(running[line.ingredientId]) || 0) * 10) / 10;
|
|
const miss = Math.max(0, Math.round((line.amount - have) * 10) / 10);
|
|
return {
|
|
...line,
|
|
pantryQty: have,
|
|
shortfall: miss,
|
|
enough: miss <= 0,
|
|
pantryUnit: def
|
|
? def.pantryUnit === 'szt' ? 'szt.' : def.pantryUnit
|
|
: line.unit,
|
|
};
|
|
});
|
|
|
|
for (const line of lines) {
|
|
const have = Number(running[line.ingredientId]) || 0;
|
|
running[line.ingredientId] = Math.max(0, Math.round((have - line.amount) * 10) / 10);
|
|
}
|
|
|
|
days.push({ date: day, dayIndex: i, items, hasShortfall: items.some((it) => !it.enough) });
|
|
}
|
|
|
|
return days;
|
|
}
|