.NET 8 backend with Zitadel JWT auth, TheMealDB integration, weekly meal plan generation, shopping list aggregation. Vue 3 + Tailwind 4 frontend with dark emerald theme, manual OIDC PKCE auth, all views implemented. Multi-stage Dockerfile with nginx reverse proxy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
92 lines
3.1 KiB
C#
92 lines
3.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using MealPlanner.Data;
|
|
using MealPlanner.Models;
|
|
|
|
namespace MealPlanner.Services;
|
|
|
|
public class ShoppingListService(AppDbContext db)
|
|
{
|
|
public async Task<List<ShoppingItem>> GetShoppingListAsync(Guid mealPlanId, string userId)
|
|
{
|
|
var plan = await db.MealPlans
|
|
.Include(p => p.Entries)
|
|
.ThenInclude(e => e.Recipe)
|
|
.ThenInclude(r => r.Ingredients)
|
|
.FirstOrDefaultAsync(p => p.Id == mealPlanId);
|
|
|
|
if (plan is null) return [];
|
|
if (plan.UserId != userId) throw new UnauthorizedAccessException();
|
|
|
|
var settings = await db.UserSettings.FindAsync(userId);
|
|
int householdSize = settings?.HouseholdSize ?? 2;
|
|
|
|
var checkedItems = await db.CheckedShoppingItems
|
|
.Where(c => c.MealPlanId == mealPlanId && c.UserId == userId)
|
|
.ToDictionaryAsync(c => c.ItemName.ToLowerInvariant(), c => c.IsChecked);
|
|
|
|
// Aggregate ingredients
|
|
var aggregated = new Dictionary<string, ShoppingItem>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var entry in plan.Entries)
|
|
{
|
|
foreach (var ing in entry.Recipe.Ingredients)
|
|
{
|
|
var key = ing.Name.Trim().ToLowerInvariant();
|
|
|
|
if (!aggregated.TryGetValue(key, out var item))
|
|
{
|
|
item = new ShoppingItem
|
|
{
|
|
Name = ing.Name.Trim(),
|
|
Unit = ing.Unit,
|
|
Category = ing.Category,
|
|
TotalAmount = null,
|
|
IsChecked = checkedItems.TryGetValue(key, out var chk) && chk,
|
|
};
|
|
aggregated[key] = item;
|
|
}
|
|
|
|
if (ing.Amount.HasValue)
|
|
{
|
|
item.TotalAmount = (item.TotalAmount ?? 0) + ing.Amount.Value * householdSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
return [.. aggregated.Values.OrderBy(i => i.Category).ThenBy(i => i.Name)];
|
|
}
|
|
|
|
public async Task<CheckedShoppingItem> ToggleCheckAsync(Guid mealPlanId, string itemName, string userId)
|
|
{
|
|
var plan = await db.MealPlans.FindAsync(mealPlanId);
|
|
if (plan is null) throw new KeyNotFoundException("Meal plan not found");
|
|
if (plan.UserId != userId) throw new UnauthorizedAccessException();
|
|
|
|
var key = itemName.ToLowerInvariant();
|
|
|
|
var existing = await db.CheckedShoppingItems
|
|
.FirstOrDefaultAsync(c => c.MealPlanId == mealPlanId && c.UserId == userId
|
|
&& c.ItemName.ToLower() == key);
|
|
|
|
if (existing is null)
|
|
{
|
|
existing = new CheckedShoppingItem
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
MealPlanId = mealPlanId,
|
|
ItemName = itemName,
|
|
UserId = userId,
|
|
IsChecked = true,
|
|
};
|
|
db.CheckedShoppingItems.Add(existing);
|
|
}
|
|
else
|
|
{
|
|
existing.IsChecked = !existing.IsChecked;
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
return existing;
|
|
}
|
|
}
|