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:
128
backend/Services/RecipeService.cs
Normal file
128
backend/Services/RecipeService.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MealPlanner.Data;
|
||||
using MealPlanner.Models;
|
||||
|
||||
namespace MealPlanner.Services;
|
||||
|
||||
public class RecipeService(AppDbContext db, TheMealDbClient mealDbClient)
|
||||
{
|
||||
public async Task<List<Recipe>> GetOwnRecipesAsync(string userId)
|
||||
{
|
||||
return await db.Recipes
|
||||
.Include(r => r.Ingredients)
|
||||
.Where(r => r.UserId == userId)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Recipe?> GetByIdAsync(Guid id)
|
||||
{
|
||||
return await db.Recipes
|
||||
.Include(r => r.Ingredients)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
}
|
||||
|
||||
public async Task<Recipe?> GetByIdOrFetchAsync(Guid id)
|
||||
{
|
||||
var recipe = await GetByIdAsync(id);
|
||||
return recipe;
|
||||
}
|
||||
|
||||
public async Task<List<Recipe>> SearchAsync(string query, string userId)
|
||||
{
|
||||
// Local own recipes
|
||||
var own = await db.Recipes
|
||||
.Include(r => r.Ingredients)
|
||||
.Where(r => r.UserId == userId && r.Title.ToLower().Contains(query.ToLower()))
|
||||
.ToListAsync();
|
||||
|
||||
// External results
|
||||
var external = await mealDbClient.SearchAsync(query);
|
||||
|
||||
// Deduplicate external by ExternalId
|
||||
var existingExternalIds = await db.Recipes
|
||||
.Where(r => r.Source == RecipeSource.TheMealDb && r.ExternalId != null)
|
||||
.Select(r => r.ExternalId!)
|
||||
.ToListAsync();
|
||||
|
||||
var newExternal = external
|
||||
.Where(r => r.ExternalId != null && !existingExternalIds.Contains(r.ExternalId))
|
||||
.ToList();
|
||||
|
||||
return [.. own, .. newExternal];
|
||||
}
|
||||
|
||||
public async Task<Recipe> CreateAsync(string userId, Recipe recipe)
|
||||
{
|
||||
recipe.Id = Guid.NewGuid();
|
||||
recipe.UserId = userId;
|
||||
recipe.Source = RecipeSource.Own;
|
||||
recipe.CreatedAt = DateTime.UtcNow;
|
||||
|
||||
foreach (var ing in recipe.Ingredients)
|
||||
{
|
||||
ing.Id = Guid.NewGuid();
|
||||
ing.RecipeId = recipe.Id;
|
||||
}
|
||||
|
||||
db.Recipes.Add(recipe);
|
||||
await db.SaveChangesAsync();
|
||||
return recipe;
|
||||
}
|
||||
|
||||
public async Task<Recipe?> UpdateAsync(Guid id, string userId, Recipe updated)
|
||||
{
|
||||
var recipe = await db.Recipes
|
||||
.Include(r => r.Ingredients)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
if (recipe is null) return null;
|
||||
if (recipe.UserId != userId) throw new UnauthorizedAccessException();
|
||||
|
||||
recipe.Title = updated.Title;
|
||||
recipe.Instructions = updated.Instructions;
|
||||
recipe.ImageUrl = updated.ImageUrl;
|
||||
|
||||
// Replace ingredients
|
||||
db.RecipeIngredients.RemoveRange(recipe.Ingredients);
|
||||
recipe.Ingredients = updated.Ingredients.Select(ing => new RecipeIngredient
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
RecipeId = recipe.Id,
|
||||
Name = ing.Name,
|
||||
Amount = ing.Amount,
|
||||
Unit = ing.Unit,
|
||||
Category = ing.Category,
|
||||
}).ToList();
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return recipe;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, string userId)
|
||||
{
|
||||
var recipe = await db.Recipes.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (recipe is null) return false;
|
||||
if (recipe.UserId != userId) throw new UnauthorizedAccessException();
|
||||
|
||||
db.Recipes.Remove(recipe);
|
||||
await db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save an external recipe to DB so it can be referenced in meal plans
|
||||
public async Task<Recipe> EnsurePersistedAsync(Recipe recipe)
|
||||
{
|
||||
if (recipe.ExternalId is not null)
|
||||
{
|
||||
var existing = await db.Recipes
|
||||
.Include(r => r.Ingredients)
|
||||
.FirstOrDefaultAsync(r => r.ExternalId == recipe.ExternalId);
|
||||
if (existing is not null) return existing;
|
||||
}
|
||||
|
||||
db.Recipes.Add(recipe);
|
||||
await db.SaveChangesAsync();
|
||||
return recipe;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user