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>
This commit is contained in:
91
backend/Services/ShoppingListService.cs
Normal file
91
backend/Services/ShoppingListService.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user