Files
DailyMeals/meal-plan-frontend-angular/src/app/core/utils/meal-planner-suggestions.util.ts
Piotr Kus f15bb7a916 * Extended functionalities
* Added different UIs
2026-06-24 21:43:40 +02:00

99 lines
3.0 KiB
TypeScript

import { MealPlanEntry } from '../models/meal-plan.models';
import { MealCategory, RecipeListItem } from '../models/recipe.models';
export const PLANNER_MEAL_CATEGORIES = [
MealCategory.Breakfast,
MealCategory.SecondBreakfast,
MealCategory.Lunch,
MealCategory.Dinner,
] as const;
export interface DayMealPlanning {
dailyTarget: number;
logged: number;
remaining: number;
slotTarget: number;
openSlots: number;
pendingCalories: number;
}
export interface PendingMealSelection {
category: MealCategory;
calories: number;
}
export function entryCaloriesTotal(entry: MealPlanEntry): number {
if (entry.calories == null) return 0;
return Math.round(entry.calories * entry.portions);
}
export function computeDayMealPlanning(
date: string,
category: MealCategory,
entries: MealPlanEntry[],
dailyTarget: number,
pending?: PendingMealSelection | null,
): DayMealPlanning {
const dayEntries = entries.filter((e) => e.planDate === date);
const categoriesWithSaved = new Set(dayEntries.map((e) => e.mealCategory));
const existingInSlot = dayEntries.find((e) => e.mealCategory === category);
let logged = dayEntries.reduce((sum, e) => sum + entryCaloriesTotal(e), 0);
const pendingCalories =
pending && pending.category === category && pending.calories > 0 ? pending.calories : 0;
if (pendingCalories > 0) {
if (existingInSlot) {
logged = logged - entryCaloriesTotal(existingInSlot) + pendingCalories;
} else {
logged += pendingCalories;
}
}
const remaining = dailyTarget - logged;
const emptyCategories = PLANNER_MEAL_CATEGORIES.filter((cat) => !categoriesWithSaved.has(cat));
const currentSlotSaved = categoriesWithSaved.has(category);
let unfilled = [...emptyCategories];
if (pendingCalories > 0 && !currentSlotSaved) {
unfilled = unfilled.filter((cat) => cat !== category);
}
let openSlots: number;
if (currentSlotSaved) {
openSlots = Math.max(1, unfilled.length + 1);
} else if (pendingCalories > 0) {
openSlots = Math.max(1, unfilled.length);
} else if (unfilled.includes(category)) {
openSlots = Math.max(1, unfilled.length);
} else {
openSlots = Math.max(1, unfilled.length + 1);
}
const slotTarget = dailyTarget > 0 ? Math.round(Math.max(0, remaining) / openSlots) : 0;
return { dailyTarget, logged, remaining, slotTarget, openSlots, pendingCalories };
}
export function rankRecipeSuggestions(
recipes: RecipeListItem[],
category: MealCategory,
slotTarget: number,
limit = 6,
): { recipe: RecipeListItem; diff: number }[] {
if (slotTarget <= 0) return [];
return recipes
.filter((r) => r.mealCategory === category && r.calories != null && r.calories > 0)
.map((recipe) => ({ recipe, diff: Math.abs(recipe.calories! - slotTarget) }))
.sort((a, b) => a.diff - b.diff || a.recipe.name.localeCompare(b.recipe.name))
.slice(0, limit);
}
export function estimateEntryCalories(recipe: RecipeListItem | null, portions: number): number | null {
if (!recipe?.calories) return null;
return Math.round(recipe.calories * portions);
}