* Extended functionalities
* Added different UIs
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user