using MealPlan.Api.Data; using MealPlan.Api.DTOs.MealPlan; using MealPlan.Api.Services.Generators; using Microsoft.EntityFrameworkCore; namespace MealPlan.Api.Services; public class MealPlanService : IMealPlanService { private readonly AppDbContext _db; private readonly IRecipeService _recipeService; public MealPlanService(AppDbContext db, IRecipeService recipeService) { _db = db; _recipeService = recipeService; } public async Task> GetEntriesAsync( Guid userId, DateOnly from, DateOnly to, CancellationToken ct = default) { return await _db.MealPlanEntries.AsNoTracking() .Include(e => e.Recipe) .Where(e => e.UserId == userId && e.PlanDate >= from && e.PlanDate <= to) .OrderBy(e => e.PlanDate) .ThenBy(e => e.MealCategory) .Select(e => new MealPlanEntryDto { Id = e.Id, PlanDate = e.PlanDate, MealCategory = e.MealCategory, RecipeId = e.RecipeId, RecipeName = e.Recipe!.Name, Portions = e.Portions, Calories = e.Recipe!.Calories, }) .ToListAsync(ct); } public async Task UpsertEntryAsync( Guid userId, UpsertMealPlanEntryDto dto, CancellationToken ct = default) { var recipeExists = await _db.Recipes.AnyAsync(r => r.Id == dto.RecipeId && r.IsActive, ct); if (!recipeExists) return null; var existing = await _db.MealPlanEntries .Include(e => e.Recipe) .FirstOrDefaultAsync( e => e.UserId == userId && e.PlanDate == dto.PlanDate && e.MealCategory == dto.MealCategory, ct); if (existing is null) { existing = new Models.MealPlanEntry { UserId = userId, PlanDate = dto.PlanDate, MealCategory = dto.MealCategory, RecipeId = dto.RecipeId, Portions = dto.Portions, CreatedAt = DateTime.UtcNow, }; _db.MealPlanEntries.Add(existing); } else { existing.RecipeId = dto.RecipeId; existing.Portions = dto.Portions; existing.UpdatedAt = DateTime.UtcNow; if (existing.Recipe is null) { await _db.Entry(existing).Reference(e => e.Recipe).LoadAsync(ct); } } await _db.SaveChangesAsync(ct); await _db.Entry(existing).Reference(e => e.Recipe).LoadAsync(ct); return new MealPlanEntryDto { Id = existing.Id, PlanDate = existing.PlanDate, MealCategory = existing.MealCategory, RecipeId = existing.RecipeId, RecipeName = existing.Recipe?.Name ?? string.Empty, Portions = existing.Portions, Calories = existing.Recipe?.Calories, }; } public async Task DeleteEntryAsync(Guid userId, int id, CancellationToken ct = default) { var entry = await _db.MealPlanEntries.FirstOrDefaultAsync(e => e.Id == id && e.UserId == userId, ct); if (entry is null) return false; _db.MealPlanEntries.Remove(entry); await _db.SaveChangesAsync(ct); return true; } public async Task> GetShoppingListAsync( Guid userId, DateOnly from, DateOnly to, CancellationToken ct = default) { var entries = await _db.MealPlanEntries.AsNoTracking() .Where(e => e.UserId == userId && e.PlanDate >= from && e.PlanDate <= to) .ToListAsync(ct); var recipeIds = entries.Select(e => e.RecipeId).Distinct().ToList(); var recipes = new List(); foreach (var id in recipeIds) { var recipe = await _recipeService.GetRecipeByIdAsync(id, ct); if (recipe is not null) recipes.Add(recipe); } var buckets = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var entry in entries) { var recipe = recipes.FirstOrDefault(r => r.Id == entry.RecipeId); if (recipe is null) continue; foreach (var ing in recipe.Ingredients) { if (IngredientScalingHelper.IsNonQuantifiedUnit(ing.Unit)) continue; var (scaledGrams, _) = IngredientScalingHelper.ScaleForDietPlan( ing.AmountGrams, ing.Unit, entry.Portions); if (scaledGrams is not > 0) continue; var key = $"g:{NormalizeName(ing.Name)}"; if (!buckets.TryGetValue(key, out var bucket)) { bucket = new ShoppingBucket(ing.Name, null); buckets[key] = bucket; } bucket.Add(recipe.Name, scaledGrams, null); } } return buckets.Values .Select(b => new ShoppingListItemDto { Name = b.Name, Unit = b.Unit, TotalGrams = b.TotalGrams, TotalAmount = b.TotalAmount, Breakdown = b.Breakdown, }) .OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .ToList(); } private static string NormalizeName(string name) => name.Trim().ToLowerInvariant(); private sealed class ShoppingBucket { public string Name { get; } public string? Unit { get; } public decimal? TotalGrams { get; private set; } public decimal? TotalAmount { get; private set; } public List Breakdown { get; } = new(); public ShoppingBucket(string name, string? unit) { Name = name; Unit = unit; } public void Add(string recipeName, decimal? grams, decimal? amount) { if (Unit is null && grams.HasValue) { TotalGrams = (TotalGrams ?? 0) + grams.Value; Breakdown.Add($"{recipeName}: {grams.Value:0.##}g"); } else if (amount.HasValue) { TotalAmount = (TotalAmount ?? 0) + amount.Value; var suffix = Unit is null ? "g" : $" {Unit}"; Breakdown.Add($"{recipeName}: {amount.Value:0.##}{suffix}"); } } } }