import type { CatalogFood, PortionUnit, Recipe, RecipeIngredient, RecipeRating, } from "./types"; export function calcFoodForAmount(food: CatalogFood, amountG: number) { const factor = amountG / 100; return { kcal: Math.round(food.kcalPer100g * factor), protein: Math.round(food.proteinPer100g * factor * 10) / 10, carbs: Math.round(food.carbsPer100g * factor * 10) / 10, fat: Math.round(food.fatPer100g * factor * 10) / 10, }; } export function unitLabel(unit: PortionUnit) { const labels: Record = { g: "g", ml: "ml", tsp: "TL", tbsp: "EL", piece: "Stück", pack: "Packung", bottle: "Flasche", cup: "Becher", slice: "Scheibe", }; return labels[unit]; } export function amountToGrams(food: CatalogFood, amount: number, unit: PortionUnit, portionId?: string) { const portion = portionId ? food.portions?.find((item) => item.id === portionId) : undefined; if (portion) return amount * (portion.gramsEquivalent / Math.max(1, portion.amount)); if (unit === "g") return amount; if (unit === "ml") return amount * (food.densityGPerMl ?? 1); if (unit === "tsp") return amount * 5 * (food.densityGPerMl ?? 1); if (unit === "tbsp") return amount * 15 * (food.densityGPerMl ?? 1); const matchingPortion = food.portions?.find((item) => item.unit === unit); if (matchingPortion) return amount * (matchingPortion.gramsEquivalent / Math.max(1, matchingPortion.amount)); return amount * food.servingG; } export function describeIngredientAmount(ingredient: RecipeIngredient) { if (ingredient.amount && ingredient.unit) { return `${ingredient.amount} ${unitLabel(ingredient.unit)}`; } return `${Math.round(ingredient.amountG)} g`; } export function calcRecipe(recipe: Recipe, foods: CatalogFood[], portions = recipe.portions) { const byId = new Map(foods.map((food) => [food.id, food])); const totals = recipe.ingredients.reduce( (sum, ingredient) => { const food = byId.get(ingredient.foodId); if (!food) return sum; const values = calcFoodForAmount(food, ingredient.amountG); return { kcal: sum.kcal + values.kcal, protein: sum.protein + values.protein, carbs: sum.carbs + values.carbs, fat: sum.fat + values.fat, }; }, { kcal: 0, protein: 0, carbs: 0, fat: 0 }, ); const divisor = Math.max(1, portions); return { kcal: Math.round(totals.kcal / divisor), protein: Math.round((totals.protein / divisor) * 10) / 10, carbs: Math.round((totals.carbs / divisor) * 10) / 10, fat: Math.round((totals.fat / divisor) * 10) / 10, }; } export function recipeMacroDistribution(totals: { protein: number; carbs: number; fat: number }) { const proteinCalories = totals.protein * 4; const carbsCalories = totals.carbs * 4; const fatCalories = totals.fat * 9; const total = Math.max(1, proteinCalories + carbsCalories + fatCalories); return { protein: Math.round((proteinCalories / total) * 100), carbs: Math.round((carbsCalories / total) * 100), fat: Math.round((fatCalories / total) * 100), }; } export function classifyRecipe(recipe: Recipe, foods: CatalogFood[]) { const byId = new Map(foods.map((food) => [food.id, food])); const usedFoods = recipe.ingredients.map((ingredient) => byId.get(ingredient.foodId)).filter(Boolean) as CatalogFood[]; const text = usedFoods.map((food) => `${food.name} ${food.category}`).join(" ").toLowerCase(); const hasMeatOrFish = /fleisch|fisch|hähnchen|huhn|pute|rind|schwein|hack|wurst|speck|salami|schinken|thunfisch|lachs|garnel|shrimp|kabeljau/.test(text); const hasAnimalProduct = hasMeatOrFish || /milch|joghurt|yoghurt|quark|skyr|käse|sahne|butter|ei\b|eier|honig|molke|whey|gelatine/.test(text); const totals = calcRecipe(recipe, foods); const distribution = recipeMacroDistribution(totals); const tags = [ hasMeatOrFish ? "mit Fleisch/Fisch" : "vegetarisch", !hasAnimalProduct ? "vegan" : undefined, totals.protein >= 25 ? "proteinreich" : undefined, totals.kcal <= 500 ? "kalorienbewusst" : undefined, distribution.fat >= 45 ? "fettreich" : undefined, distribution.carbs >= 55 ? "kohlenhydratbetont" : undefined, ].filter(Boolean) as string[]; const criteria = [ { label: "Protein sinnvoll", ok: totals.protein >= 20, detail: `${totals.protein} g pro Portion` }, { label: "Fett moderat", ok: distribution.fat <= 40, detail: `${distribution.fat} % der Makro-Kalorien` }, { label: "Kalorien alltagstauglich", ok: totals.kcal <= 750, detail: `${totals.kcal} kcal pro Portion` }, ]; return { tags, vegetarian: !hasMeatOrFish, vegan: !hasAnimalProduct, distribution, criteria, }; } export function parseNutritionLabel(text: string): Partial { const normalized = text.replace(/,/g, ".").replace(/\s+/g, " "); const read = (patterns: RegExp[]) => { for (const pattern of patterns) { const match = normalized.match(pattern); if (match?.[1]) return Number(match[1]); } return undefined; }; return { kcalPer100g: read([/(\d+(?:\.\d+)?)\s*kcal/i, /energie[^0-9]*(\d+(?:\.\d+)?)/i]), proteinPer100g: read([/(?:eiweiss|eiweiß|protein)[^0-9]*(\d+(?:\.\d+)?)/i]), carbsPer100g: read([/(?:kohlenhydrate|carbohydrate)[^0-9]*(\d+(?:\.\d+)?)/i]), fatPer100g: read([/(?:fett|fat)[^0-9]*(\d+(?:\.\d+)?)/i]), }; } export function recipeRatingSummary(recipeId: string, ratings: RecipeRating[]) { const recipeRatings = ratings.filter((rating) => rating.recipeId === recipeId); if (!recipeRatings.length) return { average: 0, count: 0 }; const average = recipeRatings.reduce((sum, rating) => sum + rating.rating, 0) / recipeRatings.length; return { average: Math.round(average * 10) / 10, count: recipeRatings.length, }; }