Files
mealplanner/backend/Services/ShoppingListService.cs
Claude f58782774b feat: complete mealplanner app (backend + frontend + deployment)
.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>
2026-04-14 19:10:10 +00:00

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;
}
}