* Extended functionalities
* Added different UIs
This commit is contained in:
197
MealPlan.Api/Services/MealPlanService.cs
Normal file
197
MealPlan.Api/Services/MealPlanService.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user