99 lines
3.0 KiB
TypeScript
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);
|
|
}
|