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); }