using MealPlan.Api.Data; using MealPlan.Api.DTOs.Recipe; using MealPlan.Api.Models; using Microsoft.EntityFrameworkCore; namespace MealPlan.Api.Services; /// /// Read-only recipe queries. All filtering is done with EF Core LINQ (parameterized); /// no raw SQL is used anywhere. /// public class RecipeService : IRecipeService { private readonly AppDbContext _db; public RecipeService(AppDbContext db) => _db = db; public async Task> GetRecipesAsync( int? category, string? search, CancellationToken ct = default) { var query = _db.Recipes.AsNoTracking().Where(r => r.IsActive); if (category is >= 0 and <= 3) { query = query.Where(r => r.MealCategory == category); } if (!string.IsNullOrWhiteSpace(search)) { var term = search.Trim(); query = query.Where(r => EF.Functions.ILike(r.Name, $"%{term}%")); } return await query .OrderBy(r => r.Name) .Select(r => new RecipeListItemDto { Id = r.Id, Name = r.Name, MealCategory = r.MealCategory, Calories = r.Calories, PrepTimeMinutes = r.PrepTimeMinutes, }) .ToListAsync(ct); } public async Task GetRecipeByIdAsync(int id, CancellationToken ct = default) { return await _db.Recipes .AsNoTracking() .Where(r => r.Id == id && r.IsActive) .Select(r => new RecipeDetailDto { Id = r.Id, Name = r.Name, MealCategory = r.MealCategory, Calories = r.Calories, Protein = r.Protein, Fat = r.Fat, Carbs = r.Carbs, PrepTimeMinutes = r.PrepTimeMinutes, Ingredients = r.Ingredients .OrderBy(i => i.SortOrder) .ThenBy(i => i.Id) .Select(i => new IngredientDto { Id = i.Id, Name = i.Name, AmountGrams = i.AmountGrams, Unit = i.Unit, SortOrder = i.SortOrder, }) .ToList(), Steps = r.Steps .OrderBy(s => s.StepNumber) .Select(s => new RecipeStepDto { Id = s.Id, StepNumber = s.StepNumber, Description = s.Description, }) .ToList(), }) .FirstOrDefaultAsync(ct); } public async Task> GetCategoriesAsync(CancellationToken ct = default) { var counts = await _db.Recipes .AsNoTracking() .Where(r => r.IsActive) .GroupBy(r => r.MealCategory) .Select(g => new { Category = g.Key, Count = g.Count() }) .ToListAsync(ct); // Always return all four categories, even when a category has zero recipes. return Enum.GetValues() .Select(category => new CategoryCountDto { Category = (int)category, Name = category.ToString(), Count = counts.FirstOrDefault(c => c.Category == (int)category)?.Count ?? 0, }) .OrderBy(c => c.Category) .ToList(); } }