Files
DailyMeals/MealPlan.Api/Services/MealPlanService.cs
Piotr Kus f15bb7a916 * Extended functionalities
* Added different UIs
2026-06-24 21:43:40 +02:00

198 lines
6.5 KiB
C#

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<IReadOnlyList<MealPlanEntryDto>> 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<MealPlanEntryDto?> 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<bool> 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<IReadOnlyList<ShoppingListItemDto>> 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<DTOs.Recipe.RecipeDetailDto>();
foreach (var id in recipeIds)
{
var recipe = await _recipeService.GetRecipeByIdAsync(id, ct);
if (recipe is not null) recipes.Add(recipe);
}
var buckets = new Dictionary<string, ShoppingBucket>(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<string> 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}");
}
}
}
}