108 lines
3.5 KiB
C#
108 lines
3.5 KiB
C#
using MealPlan.Api.Data;
|
|
using MealPlan.Api.DTOs.Recipe;
|
|
using MealPlan.Api.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace MealPlan.Api.Services;
|
|
|
|
/// <summary>
|
|
/// Read-only recipe queries. All filtering is done with EF Core LINQ (parameterized);
|
|
/// no raw SQL is used anywhere.
|
|
/// </summary>
|
|
public class RecipeService : IRecipeService
|
|
{
|
|
private readonly AppDbContext _db;
|
|
|
|
public RecipeService(AppDbContext db) => _db = db;
|
|
|
|
public async Task<IReadOnlyList<RecipeListItemDto>> 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<RecipeDetailDto?> 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<IReadOnlyList<CategoryCountDto>> 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<MealCategory>()
|
|
.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();
|
|
}
|
|
}
|