From a4c8edb572c9a047c68ad94ece13ac1d4f071f71 Mon Sep 17 00:00:00 2001 From: Francesco D'Amico Date: Sun, 30 Mar 2025 15:53:26 +0200 Subject: [PATCH 01/18] Write Controller for testing Data --- .../Controller/Category/CategoryController.cs | 43 +++++ .../Ingredient/IngredientController.cs | 6 + .../Instruction/InstructionController.cs | 6 + .../MediaFile/MediaFileController.cs | 6 + .../Controller/Recipe/RecipeController.cs | 156 ++++++++++++++++++ .../ShoppingList/ShoppingListController.cs | 6 + .../Controller/Unit/UnitController.cs | 6 + 7 files changed, 229 insertions(+) create mode 100644 Francesco.Recipes.World/Controller/Category/CategoryController.cs create mode 100644 Francesco.Recipes.World/Controller/Ingredient/IngredientController.cs create mode 100644 Francesco.Recipes.World/Controller/Instruction/InstructionController.cs create mode 100644 Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs create mode 100644 Francesco.Recipes.World/Controller/Recipe/RecipeController.cs create mode 100644 Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs create mode 100644 Francesco.Recipes.World/Controller/Unit/UnitController.cs diff --git a/Francesco.Recipes.World/Controller/Category/CategoryController.cs b/Francesco.Recipes.World/Controller/Category/CategoryController.cs new file mode 100644 index 0000000..0257ab4 --- /dev/null +++ b/Francesco.Recipes.World/Controller/Category/CategoryController.cs @@ -0,0 +1,43 @@ +namespace Francesco.Recipes.World.Controller.Category +{ + using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Repositories.Category; + using Microsoft.AspNetCore.Mvc; + + [Route("Category")] + + public class CategoryController : Controller + { + private readonly ICategoryRepository _categoryRepository; + + public CategoryController(ICategoryRepository categoryRepository) + { + _categoryRepository = categoryRepository; + } + + // GET: /Category + [HttpGet] + public async Task>> Index() + { + var categories = await _categoryRepository.GetAllCategoriesAsync(); + return View(categories); + } + + // GET: /Category/{id} + [HttpGet("{id:guid}")] + public async Task Details(Guid id) + { + var category = await _categoryRepository.GetCategoryByIdAsync(id); + return View(category); + } + + // GET: /Category/{id}/recipes + [HttpGet("{id:guid}/recipes")] + public async Task>> GetRecipesByCategory(Guid id) + { + var recipes = await _categoryRepository.GetRecipesByCategoryAsync(id); + return Ok(recipes); + } + } +} diff --git a/Francesco.Recipes.World/Controller/Ingredient/IngredientController.cs b/Francesco.Recipes.World/Controller/Ingredient/IngredientController.cs new file mode 100644 index 0000000..d59fb2a --- /dev/null +++ b/Francesco.Recipes.World/Controller/Ingredient/IngredientController.cs @@ -0,0 +1,6 @@ +namespace Francesco.Recipes.World.Controller.Ingredient +{ + public class IngredientController + { + } +} diff --git a/Francesco.Recipes.World/Controller/Instruction/InstructionController.cs b/Francesco.Recipes.World/Controller/Instruction/InstructionController.cs new file mode 100644 index 0000000..d86aadf --- /dev/null +++ b/Francesco.Recipes.World/Controller/Instruction/InstructionController.cs @@ -0,0 +1,6 @@ +namespace Francesco.Recipes.World.Controller.Instruction +{ + public class InstructionController + { + } +} diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs new file mode 100644 index 0000000..6cec16d --- /dev/null +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -0,0 +1,6 @@ +namespace Francesco.Recipes.World.Controller.MediaFile +{ + public class MediaFileController + { + } +} diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs new file mode 100644 index 0000000..5b21475 --- /dev/null +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -0,0 +1,156 @@ +namespace Francesco.Recipes.World.Controller.Recipe +{ + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Repositories.Category; + using Francesco.Recipes.World.Repositories.Ingredient; + using Francesco.Recipes.World.Repositories.Recipe; + using Francesco.Recipes.World.Repositories.Unit; + using Francesco.Recipes.World.Views.Recipe; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Rendering; + + [Route("categories/{categoryId}/Recipe")] + public class RecipeController : Controller + { + private readonly IRecipeRepository _recipeRepository; + private readonly IUnitRepository _unitRepository; + private readonly ICategoryRepository _categoryRepository; + private readonly IIngredientRepository _ingredientRepository; + + public RecipeController(IRecipeRepository recipeRepository, IUnitRepository unitRepository, ICategoryRepository categoryRepository, IIngredientRepository ingredientRepository) + { + _recipeRepository = recipeRepository; + _unitRepository = unitRepository; + _categoryRepository = categoryRepository; + _ingredientRepository = ingredientRepository; + } + + // GET: /Recipe/AddOrCreateIngredient + [HttpGet("{recipeId}/AddOrCreateIngredient")] + public async Task AddOrCreateIngredient() + { + var units = await _unitRepository.GetAllUnitsAsync(); + ViewBag.Units = new SelectList(units, "Id", "Name"); + return View(); + } + + // POST: /Recipe/AddOrCreateIngredient + [HttpPost("{recipeId}/AddOrCreateIngredient")] + [ValidateAntiForgeryToken] + public async Task AddOrCreateIngredient(Guid recipeId, string ingredientName, int quantity, Guid unitId) + { + if (quantity <= 0) + { + ModelState.AddModelError(nameof(quantity), "Die Menge muss größer als 0 sein."); + } + + if (!ModelState.IsValid) + { + var units = await _unitRepository.GetAllUnitsAsync(); + ViewBag.Units = new SelectList(units, "Id", "Name"); + return View(); + } + + await _recipeRepository.AddOrCreateIngredientToRecipeAsync(recipeId, ingredientName, quantity, unitId); + return RedirectToAction("Details", new { id = recipeId }); + } + + // GET: /categories/{categoryId}/Recipe/Create + [HttpGet("Create")] + public async Task Create(Guid categoryId) + { + var category = await _categoryRepository.GetCategoryByIdAsync(categoryId); + if (category == null) + { + return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); + } + + ViewBag.CategoryName = category.Name; + return View(); + } + + // POST: /categories/{categoryId}/Recipe/Create + [HttpPost("Create")] + [ValidateAntiForgeryToken] + public async Task Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime) + { + if (string.IsNullOrWhiteSpace(name)) + { + ModelState.AddModelError(nameof(name), "Name darf nicht leer sein."); + } + + if (servings <= 0) + { + ModelState.AddModelError(nameof(servings), "Anzahl der Portionen muss größer als 0 sein."); + } + + if (!ModelState.IsValid) + { + var category = await _categoryRepository.GetCategoryByIdAsync(categoryId); + if (category == null) + { + return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); + } + + ViewBag.CategoryName = category.Name; + return View(); + } + + var categoryEntity = await _categoryRepository.GetCategoryByIdAsync(categoryId); + if (categoryEntity == null) + { + return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); + } + + await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); + return RedirectToAction("Details", "Category", new { id = categoryId }); + } + + // GET: /categories/{categoryId}/Recipe/{recipeId}/RemoveIngredient/{ingredientId} + [HttpGet("{recipeId}/RemoveIngredient/{ingredientId}")] + public async Task RemoveIngredient(Guid categoryId, Guid recipeId, Guid ingredientId) + { + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + var ingredient = await _ingredientRepository.GetIngredientByIdAsync(ingredientId); + + if (recipe == null || ingredient == null) + { + return NotFound("Recipe or Ingredient not found."); + } + + ViewBag.RecipeId = recipeId; + ViewBag.IngredientId = ingredientId; + ViewBag.CategoryId = categoryId; + ViewBag.IngredientName = ingredient.Name; + + return View(); + } + + // POST: /categories/{categoryId}/Recipe/{recipeId}/RemoveIngredient/{ingredientId} + [HttpPost("{recipeId}/RemoveIngredient/{ingredientId}")] + [ValidateAntiForgeryToken] + public async Task RemoveIngredientConfirmed(Guid categoryId, Guid recipeId, Guid ingredientId) + { + await _recipeRepository.RemoveIngredientFromRecipeAsync(recipeId, ingredientId); + TempData["SuccessMessage"] = "Ingredient removed successfully."; + return RedirectToAction("Details", new { id = recipeId }); + } + + // GET: /categories/{categoryId}/Recipe/FilterByDifficulty + [HttpGet("FilterByDifficulty")] + public async Task FilterByDifficulty(Difficulty? difficulty) + { + var viewModel = new FilterByDifficultyViewModel + { + SelectedDifficulty = difficulty, + }; + + if (difficulty.HasValue) + { + viewModel.Recipes = (await _recipeRepository.GetRecipesByDifficultyAsync(difficulty.Value)).ToList(); + } + + return View(viewModel); + } + } +} diff --git a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs new file mode 100644 index 0000000..38fdf4b --- /dev/null +++ b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs @@ -0,0 +1,6 @@ +namespace Francesco.Recipes.World.Controller.ShoppingList +{ + public class ShoppingListController + { + } +} diff --git a/Francesco.Recipes.World/Controller/Unit/UnitController.cs b/Francesco.Recipes.World/Controller/Unit/UnitController.cs new file mode 100644 index 0000000..c0efcda --- /dev/null +++ b/Francesco.Recipes.World/Controller/Unit/UnitController.cs @@ -0,0 +1,6 @@ +namespace Francesco.Recipes.World.Controller.Unit +{ + public class UnitController + { + } +} From afde272d8a9a0c3f1a4c665423128ac4d61c206d Mon Sep 17 00:00:00 2001 From: Francesco D'Amico Date: Sun, 30 Mar 2025 15:55:19 +0200 Subject: [PATCH 02/18] Make new Migration and change logic of ingredientshoppinglist --- .../Data/FrancescosRecipesWorldDbContext.cs | 2 +- ...leRecipeIngredientShoppinglist.Designer.cs | 521 ++++++++++++++++++ ...ateNewTableRecipeIngredientShoppinglist.cs | 131 +++++ ...escosRecipesWorldDbContextModelSnapshot.cs | 44 +- 4 files changed, 678 insertions(+), 20 deletions(-) create mode 100644 Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs create mode 100644 Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.cs diff --git a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs index 67117c4..4fd6bd8 100644 --- a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs +++ b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs @@ -34,7 +34,7 @@ public DbSet Favorits => Set(); - public DbSet IngredientsShoppingLists => Set(); + public DbSet RecipeIngredientsShoppingLists => Set(); public DbSet ShoppingLists => Set(); diff --git a/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs b/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs new file mode 100644 index 0000000..6807402 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs @@ -0,0 +1,521 @@ +// +using System; +using Francesco.Recipes.World.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + [DbContext(typeof(FrancescosRecipesWorldDbContext))] + [Migration("20250324124459_CreateNewTableRecipeIngredientShoppinglist")] + partial class CreateNewTableRecipeIngredientShoppinglist + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"), + Name = "Vorspeisen & Snacks" + }, + new + { + Id = new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), + Name = "Erste Gänge" + }, + new + { + Id = new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), + Name = "Hauptgerichte" + }, + new + { + Id = new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"), + Name = "Desserts & Süßspeisen" + }, + new + { + Id = new Guid("28e39168-701a-4084-81da-d96c987c462f"), + Name = "Beilagen & Salate" + }, + new + { + Id = new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"), + Name = "Kuchen" + }, + new + { + Id = new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), + Name = "Hefegebäck & Brot" + }, + new + { + Id = new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"), + Name = "Soßen & Saucen" + }, + new + { + Id = new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), + Name = "Marmeladen & Eingemachtes" + }, + new + { + Id = new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), + Name = "Getränke" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Favorits"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeIngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeIngredientId"); + + b.HasIndex("ShoppingListId"); + + b.ToTable("RecipeIngredientsShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.ToTable("Instructions"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .HasColumnType("varbinary(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("InstructionId") + .HasColumnType("uniqueidentifier"); + + b.Property("MimeType") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("InstructionId"); + + b.HasIndex("RecipeId"); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CookingTime") + .HasColumnType("time"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Difficulty") + .HasColumnType("int"); + + b.Property("FavoritId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsFavorite") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PreparationTime") + .HasColumnType("time"); + + b.Property("Servings") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("FavoritId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UnitId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeId"); + + b.HasIndex("UnitId"); + + b.ToTable("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("ShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Units"); + + b.HasData( + new + { + Id = new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), + Name = "liter", + Symbol = "l" + }, + new + { + Id = new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), + Name = "gramm", + Symbol = "g" + }, + new + { + Id = new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), + Name = "kilogramm", + Symbol = "kg" + }, + new + { + Id = new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), + Name = "stücke", + Symbol = "stk" + }, + new + { + Id = new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), + Name = "blatt", + Symbol = "blatt" + }, + new + { + Id = new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), + Name = "messerspitze", + Symbol = "msp" + }, + new + { + Id = new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), + Name = "stange", + Symbol = "stange" + }, + new + { + Id = new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"), + Name = "bund", + Symbol = "bund" + }, + new + { + Id = new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), + Name = "zehe", + Symbol = "zehe" + }, + new + { + Id = new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), + Name = "teelöffel", + Symbol = "TL" + }, + new + { + Id = new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), + Name = "esslöffel", + Symbol = "EL" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", null) + .WithMany("IngredientShoppingLists") + .HasForeignKey("IngredientId"); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", "RecipeIngredient") + .WithMany() + .HasForeignKey("RecipeIngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") + .WithMany("RecipeIngredientShoppingLists") + .HasForeignKey("ShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RecipeIngredient"); + + b.Navigation("ShoppingList"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("Instructions") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", "Instruction") + .WithMany("MediaFiles") + .HasForeignKey("InstructionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("MediaFiles") + .HasForeignKey("RecipeId"); + + b.Navigation("Instruction"); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", null) + .WithMany("Recipes") + .HasForeignKey("CategoryId"); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit") + .WithMany("Recipe") + .HasForeignKey("FavoritId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Favorit"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient") + .WithMany("RecipeIngredients") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("RecipeIngredients") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit") + .WithMany("RecipeIngredient") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("Recipe"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => + { + b.Navigation("Recipes"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => + { + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => + { + b.Navigation("IngredientShoppingLists"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.Navigation("MediaFiles"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.Navigation("Instructions"); + + b.Navigation("MediaFiles"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => + { + b.Navigation("RecipeIngredientShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => + { + b.Navigation("RecipeIngredient"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.cs b/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.cs new file mode 100644 index 0000000..94df359 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.cs @@ -0,0 +1,131 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + /// + public partial class CreateNewTableRecipeIngredientShoppinglist : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropTable( + name: "IngredientsShoppingLists"); + + migrationBuilder.AlterColumn( + name: "Number", + table: "Instructions", + type: "int", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.CreateTable( + name: "RecipeIngredientsShoppingLists", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ShoppingListId = table.Column(type: "uniqueidentifier", nullable: false), + RecipeIngredientId = table.Column(type: "uniqueidentifier", nullable: false), + IngredientId = table.Column(type: "uniqueidentifier", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_RecipeIngredientsShoppingLists", x => x.Id); + table.ForeignKey( + name: "FK_RecipeIngredientsShoppingLists_Ingredients_IngredientId", + column: x => x.IngredientId, + principalTable: "Ingredients", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_RecipeIngredientsShoppingLists_RecipeIngredients_RecipeIngredientId", + column: x => x.RecipeIngredientId, + principalTable: "RecipeIngredients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId", + column: x => x.ShoppingListId, + principalTable: "ShoppingLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RecipeIngredientsShoppingLists_IngredientId", + table: "RecipeIngredientsShoppingLists", + column: "IngredientId"); + + migrationBuilder.CreateIndex( + name: "IX_RecipeIngredientsShoppingLists_RecipeIngredientId", + table: "RecipeIngredientsShoppingLists", + column: "RecipeIngredientId"); + + migrationBuilder.CreateIndex( + name: "IX_RecipeIngredientsShoppingLists_ShoppingListId", + table: "RecipeIngredientsShoppingLists", + column: "ShoppingListId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropTable( + name: "RecipeIngredientsShoppingLists"); + + migrationBuilder.AlterColumn( + name: "Number", + table: "Instructions", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.CreateTable( + name: "IngredientsShoppingLists", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + IngredientId = table.Column(type: "uniqueidentifier", nullable: false), + ShoppinglistId = table.Column(type: "uniqueidentifier", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_IngredientsShoppingLists", x => x.Id); + table.ForeignKey( + name: "FK_IngredientsShoppingLists_Ingredients_IngredientId", + column: x => x.IngredientId, + principalTable: "Ingredients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_IngredientsShoppingLists_ShoppingLists_ShoppinglistId", + column: x => x.ShoppinglistId, + principalTable: "ShoppingLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_IngredientsShoppingLists_IngredientId", + table: "IngredientsShoppingLists", + column: "IngredientId"); + + migrationBuilder.CreateIndex( + name: "IX_IngredientsShoppingLists_ShoppinglistId", + table: "IngredientsShoppingLists", + column: "ShoppinglistId"); + } + } +} diff --git a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs index 130fbf5..6b432d8 100644 --- a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs +++ b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs @@ -1,10 +1,8 @@ // - using Francesco.Recipes.World.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; - #nullable disable namespace Francesco.Recipes.World.Migrations @@ -117,25 +115,30 @@ namespace Francesco.Recipes.World.Migrations b.ToTable("Ingredients"); }); - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.IngredientsShoppingList", b => + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("IngredientId") + b.Property("IngredientId") .HasColumnType("uniqueidentifier"); - b.Property("ShoppinglistId") + b.Property("RecipeIngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShoppingListId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.HasIndex("IngredientId"); - b.HasIndex("ShoppinglistId"); + b.HasIndex("RecipeIngredientId"); - b.ToTable("IngredientsShoppingLists"); + b.HasIndex("ShoppingListId"); + + b.ToTable("RecipeIngredientsShoppingLists"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => @@ -148,9 +151,8 @@ namespace Francesco.Recipes.World.Migrations .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("Number") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("Number") + .HasColumnType("int"); b.Property("RecipeId") .HasColumnType("uniqueidentifier"); @@ -374,23 +376,27 @@ namespace Francesco.Recipes.World.Migrations }); }); - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.IngredientsShoppingList", b => + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient") + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", null) .WithMany("IngredientShoppingLists") - .HasForeignKey("IngredientId") + .HasForeignKey("IngredientId"); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", "RecipeIngredient") + .WithMany() + .HasForeignKey("RecipeIngredientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "Shoppinglist") - .WithMany("IngredientsShoppingLists") - .HasForeignKey("ShoppinglistId") + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") + .WithMany("RecipeIngredientShoppingLists") + .HasForeignKey("ShoppingListId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Ingredient"); + b.Navigation("RecipeIngredient"); - b.Navigation("Shoppinglist"); + b.Navigation("ShoppingList"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => @@ -496,7 +502,7 @@ namespace Francesco.Recipes.World.Migrations modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => { - b.Navigation("IngredientsShoppingLists"); + b.Navigation("RecipeIngredientShoppingLists"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => From b2ba51d2970dacaadebfaf5cb0810e390b3b3825 Mon Sep 17 00:00:00 2001 From: Francesco D'Amico Date: Sun, 30 Mar 2025 15:56:01 +0200 Subject: [PATCH 03/18] update model --- .../Models/BackendModels/Ingredient/Ingredient.cs | 2 +- .../IngredientsShoppingList.cs | 14 -------------- .../Models/BackendModels/Recipe/Recipe.cs | 3 +++ .../RecipeIngredientShoppingList.cs | 14 ++++++++++++++ .../BackendModels/Shoppinglist/ShoppingList.cs | 2 +- 5 files changed, 19 insertions(+), 16 deletions(-) delete mode 100644 Francesco.Recipes.World/Models/BackendModels/IngredientShoppingList/IngredientsShoppingList.cs create mode 100644 Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs diff --git a/Francesco.Recipes.World/Models/BackendModels/Ingredient/Ingredient.cs b/Francesco.Recipes.World/Models/BackendModels/Ingredient/Ingredient.cs index c4c43c3..ccc9ba1 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Ingredient/Ingredient.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Ingredient/Ingredient.cs @@ -11,6 +11,6 @@ public virtual ICollection RecipeIngredients { get; set; } = new List(); - public virtual ICollection IngredientShoppingLists { get; set; } = new List(); + public virtual ICollection IngredientShoppingLists { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Models/BackendModels/IngredientShoppingList/IngredientsShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/IngredientShoppingList/IngredientsShoppingList.cs deleted file mode 100644 index 78f92d7..0000000 --- a/Francesco.Recipes.World/Models/BackendModels/IngredientShoppingList/IngredientsShoppingList.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList -{ - using Francesco.Recipes.World.Models.BackendModels.Ingredient; - using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; - - public class IngredientsShoppingList - { - public Guid Id { get; set; } - - public ShoppingList Shoppinglist { get; set; } = new (); - - public Ingredient Ingredient { get; set; } = new (); - } -} diff --git a/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs index da673d7..977b9ad 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs @@ -1,5 +1,6 @@ namespace Francesco.Recipes.World.Models.BackendModels.Recipe; +using Francesco.Recipes.World.Models.BackendModels.Category; using Francesco.Recipes.World.Models.BackendModels.Favorit; using Francesco.Recipes.World.Models.BackendModels.Instruction; using Francesco.Recipes.World.Models.BackendModels.MediaFile; @@ -34,4 +35,6 @@ public class Recipe : ITimeStampedEntity public virtual ICollection MediaFiles { get; set; } = new List(); public virtual Favorit Favorit { get; set; } = new (); + + public virtual Category Category { get; set; } = new (); } diff --git a/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs new file mode 100644 index 0000000..1878caf --- /dev/null +++ b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs @@ -0,0 +1,14 @@ +namespace Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList +{ + using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; + using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; + + public class RecipeIngredientShoppingList + { + public Guid Id { get; set; } + + public virtual ShoppingList ShoppingList { get; set; } = new (); + + public virtual RecipeIngredient RecipeIngredient { get; set; } = new (); + } +} diff --git a/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs index 71fc7e7..c8fd12f 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs @@ -10,6 +10,6 @@ public DateTime? ModifiedAt { get; set; } - public virtual ICollection IngredientsShoppingLists { get; set; } = new List(); + public virtual ICollection RecipeIngredientShoppingLists { get; set; } = new List(); } } From c48d1bb5b23410fe778e494e01b2ea63273b4ffd Mon Sep 17 00:00:00 2001 From: Francesco D'Amico Date: Sun, 30 Mar 2025 15:56:17 +0200 Subject: [PATCH 04/18] make views for testing data --- .../Views/Category/Details.cshtml | 23 +++++++++ .../Views/Category/Index.cshtml | 28 +++++++++++ .../Views/Home/Index.cshtml | 8 --- .../Views/Recipe/AddOrCreateIngredient.cshtml | 29 +++++++++++ .../Views/Recipe/Create.cshtml | 50 +++++++++++++++++++ .../Views/Recipe/FilterByDifficulty.cshtml | 36 +++++++++++++ .../Recipe/FilterByDifficultyViewModel.cs | 12 +++++ .../Views/Recipe/RemoveIngredient.cshtml | 23 +++++++++ 8 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 Francesco.Recipes.World/Views/Category/Details.cshtml create mode 100644 Francesco.Recipes.World/Views/Category/Index.cshtml delete mode 100644 Francesco.Recipes.World/Views/Home/Index.cshtml create mode 100644 Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml create mode 100644 Francesco.Recipes.World/Views/Recipe/Create.cshtml create mode 100644 Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml create mode 100644 Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs create mode 100644 Francesco.Recipes.World/Views/Recipe/RemoveIngredient.cshtml diff --git a/Francesco.Recipes.World/Views/Category/Details.cshtml b/Francesco.Recipes.World/Views/Category/Details.cshtml new file mode 100644 index 0000000..ba13edc --- /dev/null +++ b/Francesco.Recipes.World/Views/Category/Details.cshtml @@ -0,0 +1,23 @@ +@model Francesco.Recipes.World.Models.BackendModels.Category.Category + +@{ + ViewData["Title"] = "Category Details"; +} + +

Category Details

+ +
+

Category

+
+
+
+ Name +
+
+ @Model.Name +
+
+
+ diff --git a/Francesco.Recipes.World/Views/Category/Index.cshtml b/Francesco.Recipes.World/Views/Category/Index.cshtml new file mode 100644 index 0000000..9aa30c6 --- /dev/null +++ b/Francesco.Recipes.World/Views/Category/Index.cshtml @@ -0,0 +1,28 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Categories"; +} + +

Categories

+ + + + + + + + + + @foreach (var category in Model) + { + + + + + } + +
NameActions
@category.Name + Details + Recipes +
diff --git a/Francesco.Recipes.World/Views/Home/Index.cshtml b/Francesco.Recipes.World/Views/Home/Index.cshtml deleted file mode 100644 index bcfd79a..0000000 --- a/Francesco.Recipes.World/Views/Home/Index.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@{ - ViewData["Title"] = "Home Page"; -} - -
-

Welcome

-

Learn about building Web apps with ASP.NET Core.

-
diff --git a/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml new file mode 100644 index 0000000..c72fdcf --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml @@ -0,0 +1,29 @@ +@{ + ViewData["Title"] = "Add or Create Ingredient to Recipe"; +} + +

@ViewData["Title"]

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +@section Scripts { + +} diff --git a/Francesco.Recipes.World/Views/Recipe/Create.cshtml b/Francesco.Recipes.World/Views/Recipe/Create.cshtml new file mode 100644 index 0000000..8b1e201 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/Create.cshtml @@ -0,0 +1,50 @@ +@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe +@using Francesco.Recipes.World.Models.BackendModels.Recipe +@{ + ViewData["Title"] = "Create Recipe"; +} + +

Create Recipe

+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ +@section Scripts { + @{ + await Html.RenderPartialAsync("_ValidationScriptsPartial"); + } +} + diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml new file mode 100644 index 0000000..6b9ff3b --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml @@ -0,0 +1,36 @@ + @model Francesco.Recipes.World.Views.Recipe.FilterByDifficultyViewModel + @using Francesco.Recipes.World.Models.BackendModels.Recipe + +@{ + ViewData["Title"] = "Filter Recipes by Difficulty"; +} + +

Filter Recipes by Difficulty

+ +
+
+ + + +
+ +
+ +@if (Model.Recipes != null && Model.Recipes.Any()) +{ +

Filtered Recipes

+
    + @foreach (var recipe in Model.Recipes) + { +
  • @recipe.Name - @recipe.Difficulty
  • + } +
+} +else +{ +

No recipes found for the selected difficulty.

+} + +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") + } diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs b/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs new file mode 100644 index 0000000..94f71f6 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs @@ -0,0 +1,12 @@ +namespace Francesco.Recipes.World.Views.Recipe +{ + using System.Collections.Generic; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + + public class FilterByDifficultyViewModel + { + public Difficulty? SelectedDifficulty { get; set; } + + public List Recipes { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Views/Recipe/RemoveIngredient.cshtml b/Francesco.Recipes.World/Views/Recipe/RemoveIngredient.cshtml new file mode 100644 index 0000000..ca32396 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/RemoveIngredient.cshtml @@ -0,0 +1,23 @@ +@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe + +@{ + ViewData["Title"] = "Remove Ingredient"; +} + +

Remove Ingredient

+ +

Are you sure you want to remove the ingredient '@ViewBag.IngredientName' from this recipe?

+ +
+ + + +
+ + Cancel +
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} From 2acb4b4117254126be303af44abf22a796c18c94 Mon Sep 17 00:00:00 2001 From: Francesco D'Amico Date: Sun, 30 Mar 2025 15:56:39 +0200 Subject: [PATCH 05/18] change some logic to the repositories --- .../Category/CategoryRepository.cs | 45 +++++ .../Category/ICategoryRepository.cs | 17 ++ .../Ingredient/IIngredientRepository.cs | 21 +++ .../Ingredient/IngredientRepository.cs | 109 +++++++++++ .../Instruction/IInstructionRepository.cs | 9 + .../Instruction/InstructionRepository.cs | 21 +++ .../MediaFile/IMediaFileRepository.cs | 13 ++ .../MediaFile/MediaFileRepository.cs | 101 ++++++++++ .../Repositories/Recipe/IRecipeRepository.cs | 23 +++ .../Repositories/Recipe/RecipeRepository.cs | 175 ++++++++++++++++++ .../ShoppingList/IShoppingListRepository.cs | 19 ++ .../ShoppingList/ShoppingListRepository.cs | 106 +++++++++++ .../Repositories/Unit/IUnitRepository.cs | 13 ++ .../Repositories/Unit/UnitRepository.cs | 43 +++++ 14 files changed, 715 insertions(+) create mode 100644 Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs diff --git a/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs new file mode 100644 index 0000000..6e372f8 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs @@ -0,0 +1,45 @@ +namespace Francesco.Recipes.World.Repositories.Category +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Microsoft.EntityFrameworkCore; + + public class CategoryRepository : ICategoryRepository + { + private readonly FrancescosRecipesWorldDbContext _context; + + public CategoryRepository(FrancescosRecipesWorldDbContext context) + { + _context = context; + } + + public async Task GetCategoryByIdAsync(Guid categoryId) + { + var category = await _context.Categories.FindAsync(categoryId); + return category ?? throw new InvalidDataException($"Category {categoryId} not found."); + } + + public async Task> GetAllCategoriesAsync() + { + return await _context.Categories.ToListAsync(); + } + + public async Task> GetRecipesByCategoryAsync(Guid categoryId) + { + var category = await _context.Categories + .Include(c => c.Recipes) + .FirstOrDefaultAsync(c => c.Id == categoryId); + + if (category == null) + { + throw new InvalidDataException($"Category {categoryId} not found."); + } + + return category.Recipes; + } + } +} diff --git a/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs b/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs new file mode 100644 index 0000000..04491aa --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs @@ -0,0 +1,17 @@ +namespace Francesco.Recipes.World.Repositories.Category +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + + public interface ICategoryRepository + { + Task GetCategoryByIdAsync(Guid categoryId); + + Task> GetAllCategoriesAsync(); + + Task> GetRecipesByCategoryAsync(Guid categoryId); + } +} diff --git a/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs b/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs new file mode 100644 index 0000000..013e2d1 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs @@ -0,0 +1,21 @@ +namespace Francesco.Recipes.World.Repositories.Ingredient +{ + using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; + + public interface IIngredientRepository + { + Task CreateIngredientToRecipeAsync(Recipe recipe, string ingredientName); + + Task UpdateIngredientAsync(Ingredient ingredient); + + Task> GetIngredientsByRecipeIdAsync(Guid recipeId); + + Task> GetIngredientsByNameAsync(string name); + + Task RemoveIngredientFromRecipeAsync(Recipe recipe, Guid ingredientId); + + Task GetIngredientByIdAsync(Guid ingredientId); + } +} diff --git a/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs b/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs new file mode 100644 index 0000000..f573a81 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs @@ -0,0 +1,109 @@ +namespace Francesco.Recipes.World.Repositories.Ingredient +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; + using Microsoft.EntityFrameworkCore; + + public class IngredientRepository : IIngredientRepository + { + private readonly FrancescosRecipesWorldDbContext _context; + + public IngredientRepository( + FrancescosRecipesWorldDbContext context) + { + _context = context; + } + + public async Task CreateIngredientToRecipeAsync(Recipe recipe, string ingredientName) + { + if (recipe is null) + { + throw new ArgumentException("Recipe not found", nameof(recipe)); + } + + var newIngredient = new Ingredient + { + Id = Guid.NewGuid(), + Name = ingredientName, + }; + + var recipeIngredient = new RecipeIngredient + { + Id = Guid.NewGuid(), + Recipe = recipe, + Ingredient = newIngredient, + Quantity = 1, + }; + + _context.Ingredients.Add(newIngredient); + _context.RecipeIngredients.Add(recipeIngredient); + await _context.SaveChangesAsync(); + + return newIngredient; + } + + public async Task RemoveIngredientFromRecipeAsync(Recipe recipe, Guid ingredientId) + { + if (recipe == null) + { + throw new ArgumentNullException(nameof(recipe)); + } + + var ingredientToRemove = recipe.RecipeIngredients.FirstOrDefault(i => i.Id == ingredientId); + + if (ingredientToRemove != null) + { + recipe.RecipeIngredients.Remove(ingredientToRemove); + await _context.SaveChangesAsync(); + } + } + + public async Task> GetIngredientsByNameAsync(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return await _context.Ingredients.ToListAsync(); + } + + return await _context.Ingredients + .Where(i => i.Name.ToLower().Contains(name.ToLower())) + .ToListAsync(); + } + + public async Task> GetIngredientsByRecipeIdAsync(Guid recipeId) + { + return await _context.RecipeIngredients + .Include(ri => ri.Ingredient) + .Include(ri => ri.Unit) + .Where(ri => ri.Recipe.Id == recipeId) + .ToListAsync(); + } + + public async Task UpdateIngredientAsync(Ingredient ingredient) + { + if (ingredient == null) + { + throw new ArgumentNullException(nameof(ingredient)); + } + + var existingIngredient = await _context.Ingredients.FindAsync(ingredient.Id); + if (existingIngredient == null) + { + throw new InvalidOperationException($"Ingredient with ID {ingredient.Id} not found."); + } + + existingIngredient.Name = ingredient.Name; + + _context.Ingredients.Update(existingIngredient); + await _context.SaveChangesAsync(); + } + + public async Task GetIngredientByIdAsync(Guid ingredientId) + { + var ingredient = await _context.Ingredients.FindAsync(ingredientId); + return ingredient ?? throw new InvalidDataException($"Address {ingredientId} not found."); + } + } +} diff --git a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs new file mode 100644 index 0000000..7969570 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs @@ -0,0 +1,9 @@ +namespace Francesco.Recipes.World.Repositories.Instruction +{ + using Francesco.Recipes.World.Models.BackendModels.Instruction; + + public interface IInstructionRepository + { + Task GetInstructionAsync(Guid instructionId); + } +} diff --git a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs new file mode 100644 index 0000000..b772813 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs @@ -0,0 +1,21 @@ +namespace Francesco.Recipes.World.Repositories.Instruction +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.Instruction; + + public class InstructionRepository : IInstructionRepository + { + private readonly FrancescosRecipesWorldDbContext _context; + + public InstructionRepository(FrancescosRecipesWorldDbContext context) + { + _context = context; + } + + public async Task GetInstructionAsync(Guid instructionId) + { + var instruction = await _context.Instructions.FindAsync(instructionId); + return instruction ?? throw new InvalidDataException($"Instruction {instructionId} not found."); + } + } +} diff --git a/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs new file mode 100644 index 0000000..0fc5d91 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs @@ -0,0 +1,13 @@ +namespace Francesco.Recipes.World.Repositories.MediaFile +{ + public interface IMediaFileRepository + { + Task ReplaceInstructionImageAsync(Guid instructionId, IFormFile? newPhoto); + + Task UploadInstructionImageAsync(Guid instructionId, IFormFile? photo); + + Task ReplaceRecipeImageAsync(Guid recipeId, IFormFile? newPhoto); + + Task UploadRecipeImageAsync(Guid recipeId, IFormFile? photo); + } +} diff --git a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs new file mode 100644 index 0000000..742e4c4 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -0,0 +1,101 @@ +namespace Francesco.Recipes.World.Repositories.MediaFile +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.MediaFile; + using Francesco.Recipes.World.Repositories.Instruction; + using Francesco.Recipes.World.Repositories.Recipe; + + public class MediaFileRepository : IMediaFileRepository + { + private readonly IInstructionRepository _instructionRepository; + private readonly FrancescosRecipesWorldDbContext _context; + private readonly IRecipeRepository _recipeRepository; + + public MediaFileRepository(IInstructionRepository instructionRepository, FrancescosRecipesWorldDbContext context, IRecipeRepository recipeRepository) + { + _instructionRepository = instructionRepository; + _context = context; + _recipeRepository = recipeRepository; + } + + public async Task ReplaceInstructionImageAsync(Guid instructionId, IFormFile? newPhoto) + { + if (newPhoto is null) + { + throw new ArgumentNullException(nameof(newPhoto)); + } + + var instruction = await _instructionRepository.GetInstructionAsync(instructionId); + _context.RemoveRange(instruction.MediaFiles); + await _context.SaveChangesAsync(); + + await UploadInstructionImageAsync(instructionId, newPhoto); + } + + public async Task ReplaceRecipeImageAsync(Guid recipeId, IFormFile? newPhoto) + { + if (newPhoto is null) + { + throw new ArgumentNullException(nameof(newPhoto)); + } + + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + _context.RemoveRange(recipe.MediaFiles); + await _context.SaveChangesAsync(); + + await UploadInstructionImageAsync(recipeId, newPhoto); + } + + public async Task UploadInstructionImageAsync(Guid instructionId, IFormFile? photo) + { + if (photo is null) + { + throw new ArgumentNullException(nameof(photo)); + } + + var instruction = await _instructionRepository.GetInstructionAsync(instructionId); + + using (var memoryStream = new MemoryStream()) + { + await photo.CopyToAsync(memoryStream); + + var instructionImage = new MediaFile + { + FileName = photo.FileName, + MimeType = photo.ContentType, + Data = memoryStream.ToArray(), + Instruction = instruction, + }; + + _context.Add(instructionImage); + await _context.SaveChangesAsync(); + } + } + + public async Task UploadRecipeImageAsync(Guid recipeId, IFormFile? photo) + { + if (photo is null) + { + throw new ArgumentNullException(nameof(photo)); + } + + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + + using (var memoryStream = new MemoryStream()) + { + await photo.CopyToAsync(memoryStream); + + var recipeImage = new MediaFile + { + FileName = photo.FileName, + MimeType = photo.ContentType, + Data = memoryStream.ToArray(), + Recipe = recipe, + }; + + _context.Add(recipeImage); + await _context.SaveChangesAsync(); + } + } + } +} diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs new file mode 100644 index 0000000..43a652d --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -0,0 +1,23 @@ +namespace Francesco.Recipes.World.Repositories.Recipe +{ + using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.Unit; + + public interface IRecipeRepository + { + Task GetRecipeAsync(Guid recipeId); + + Task AddOrCreateIngredientToRecipeAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId); + + Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId); + + Task> GetRecipesByNameOrIngredientAsync(string name, string ingredient); + + Task> GetRecipesByDifficultyAsync(Difficulty difficulty); + + Task AddUnitToRecipeAsync(string name, string symbol); + + Task CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime); + } +} diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs new file mode 100644 index 0000000..300c467 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -0,0 +1,175 @@ +namespace Francesco.Recipes.World.Repositories.Recipe +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; + using Francesco.Recipes.World.Models.BackendModels.Unit; + using Francesco.Recipes.World.Repositories.Ingredient; + using Francesco.Recipes.World.Repositories.Unit; + using Microsoft.EntityFrameworkCore; + + public class RecipeRepository : IRecipeRepository + { + private readonly FrancescosRecipesWorldDbContext _context; + private readonly IIngredientRepository _ingredientRepository; + private readonly IUnitRepository _unitRepository; + + public RecipeRepository( + FrancescosRecipesWorldDbContext context, IIngredientRepository ingredientRepository, IUnitRepository unitRepository) + { + _context = context; + _ingredientRepository = ingredientRepository; + _unitRepository = unitRepository; + } + + public async Task GetRecipeAsync(Guid recipeId) + { + var recipe = await _context.Recipes.FindAsync(recipeId); + return recipe ?? throw new InvalidDataException($"Address {recipeId} not found."); + } + + public async Task AddOrCreateIngredientToRecipeAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId) + { + var recipe = await GetRecipeAsync(recipeId); + var unit = await _unitRepository.GetUnitByIdAsync(unitId); + + if (quantity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(quantity), "Die Menge muss größer als 0 sein."); + } + + var ingredients = await _ingredientRepository.GetIngredientsByNameAsync(ingredientName); + Ingredient ingredient; + + if (ingredients == null || !ingredients.Any()) + { + ingredient = new Ingredient + { + Id = Guid.NewGuid(), + Name = ingredientName, + }; + _context.Ingredients.Add(ingredient); + await _context.SaveChangesAsync(); + } + else + { + ingredient = ingredients.First(); + } + + var existingEntry = await _context.RecipeIngredients + .FirstOrDefaultAsync(ri => ri.Recipe.Id == recipeId && ri.Ingredient.Id == ingredient.Id); + if (existingEntry != null) + { + throw new InvalidOperationException("Das Rezept enthält diese Zutat bereits."); + } + + var recipeIngredient = new RecipeIngredient + { + Recipe = recipe, + Ingredient = ingredient, + Unit = unit, + Quantity = quantity, + }; + _context.Add(recipeIngredient); + await _context.SaveChangesAsync(); + } + + public async Task CreateRecipeForCategoryAsync( + Category category, + string name, + string description, + Difficulty difficulty, + int servings, + TimeSpan preparationTime, + TimeSpan cookingTime) + { + if (category == null) + { + throw new ArgumentNullException(nameof(category), "Category cannot be null."); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name), "Name cannot be empty."); + } + + if (servings <= 0) + { + throw new ArgumentOutOfRangeException(nameof(servings), "Servings must be greater than 0."); + } + + var recipe = new Recipe + { + Id = Guid.NewGuid(), + Name = name, + Description = description, + Difficulty = difficulty, + Servings = servings, + PreparationTime = preparationTime, + CookingTime = cookingTime, + CreatedAt = DateTime.UtcNow, + Category = category, + }; + + _context.Recipes.Add(recipe); + await _context.SaveChangesAsync(); + + return recipe; + } + + public async Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId) + { + var recipe = await GetRecipeAsync(recipeId); + var recipeIngredient = await _context.RecipeIngredients + .FirstOrDefaultAsync(ri => ri.Recipe.Id == recipeId && ri.Ingredient.Id == ingredientId); + + if (recipeIngredient == null) + { + throw new ArgumentException("Diese Zutat ist nicht mit dem Rezept verknüpft."); + } + + _context.RecipeIngredients.Remove(recipeIngredient); + await _context.SaveChangesAsync(); + } + + public async Task> GetRecipesByNameOrIngredientAsync(string name, string ingredient) + { + var query = _context.Recipes + .Include(r => r.RecipeIngredients) + .ThenInclude(ri => ri.Ingredient) + .AsQueryable(); + + if (!string.IsNullOrWhiteSpace(name)) + { + query = query.Where(r => r.Name.Contains(name, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrWhiteSpace(ingredient)) + { + var ingredientMatches = await _ingredientRepository.GetIngredientsByNameAsync(ingredient); + var ingredientIds = ingredientMatches.Select(i => i.Id).ToList(); + + if (ingredientIds.Any()) + { + query = query.Where(r => r.RecipeIngredients.Any(ri => ingredientIds.Contains(ri.Ingredient.Id))); + } + } + + return await query.ToListAsync(); + } + + public async Task> GetRecipesByDifficultyAsync(Difficulty difficulty) + { + return await _context.Recipes + .Where(r => r.Difficulty == difficulty) + .ToListAsync(); + } + + public async Task AddUnitToRecipeAsync(string name, string symbol) + { + return await _unitRepository.AddUnitAsync(name, symbol); + } + } +} diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs new file mode 100644 index 0000000..2205dbc --- /dev/null +++ b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs @@ -0,0 +1,19 @@ + +namespace Francesco.Recipes.World.Repositories.ShoppingList +{ + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; + + public interface IShoppingListRepository + { + Task AddIngredientToShoppingListAsync(Guid recipeIngredientId, Guid shoppingListId); + + Task RemoveRecipeIngredientFromShoppingListAsync(Guid recipeIngredientId); + + Task RemoveRecipeFromShoppingListIfEmptyAsync(Guid recipeId, Guid shoppingListId); + + Task> GetShoppingListsByIngredientOfRecipeAsync(Guid ingredientId, Guid recipeId); + + Task GetRecipeByNameAndImageAsync(string recipeName, string imageFileName); + } +} diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs new file mode 100644 index 0000000..5609143 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs @@ -0,0 +1,106 @@ +namespace Francesco.Recipes.World.Repositories.ShoppingList +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; + using Microsoft.EntityFrameworkCore; + + public class ShoppingListRepository : IShoppingListRepository + { + private readonly FrancescosRecipesWorldDbContext _context; + + public ShoppingListRepository(FrancescosRecipesWorldDbContext context) + { + _context = context; + } + + public async Task AddIngredientToShoppingListAsync(Guid recipeIngredientId, Guid shoppingListId) + { + var existingEntry = await _context.RecipeIngredientsShoppingLists + .FirstOrDefaultAsync(risl => risl.RecipeIngredient.Id == recipeIngredientId && risl.ShoppingList.Id == shoppingListId); + + if (existingEntry != null) + { + return; + } + + var recipeIngredient = await _context.RecipeIngredients + .FirstOrDefaultAsync(ri => ri.Id == recipeIngredientId); + + if (recipeIngredient == null) + { + throw new InvalidOperationException($"Recipe ingredient with ID {recipeIngredientId} not found."); + } + + var shoppingList = await _context.ShoppingLists + .FirstOrDefaultAsync(sl => sl.Id == shoppingListId); + + if (shoppingList == null) + { + throw new InvalidOperationException($"Shopping list with ID {shoppingListId} not found."); + } + + var newEntry = new RecipeIngredientShoppingList + { + RecipeIngredient = recipeIngredient, + ShoppingList = shoppingList, + }; + + _context.RecipeIngredientsShoppingLists.Add(newEntry); + await _context.SaveChangesAsync(); + } + + public async Task RemoveRecipeIngredientFromShoppingListAsync(Guid recipeIngredientId) + { + var entry = await _context.RecipeIngredientsShoppingLists + .FirstOrDefaultAsync(risl => risl.RecipeIngredient.Id == recipeIngredientId); + + if (entry != null) + { + _context.RecipeIngredientsShoppingLists.Remove(entry); + await _context.SaveChangesAsync(); + } + } + + public async Task RemoveRecipeFromShoppingListIfEmptyAsync(Guid recipeId, Guid shoppingListId) + { + var hasIngredients = await _context.RecipeIngredientsShoppingLists + .AnyAsync(risl => risl.RecipeIngredient.Recipe.Id == recipeId && risl.ShoppingList.Id == shoppingListId); + + if (!hasIngredients) + { + var shoppingList = await _context.ShoppingLists + .Include(sl => sl.RecipeIngredientShoppingLists) + .FirstOrDefaultAsync(sl => sl.Id == shoppingListId); + + if (shoppingList != null) + { + var recipeToRemove = shoppingList.RecipeIngredientShoppingLists + .FirstOrDefault(risl => risl.RecipeIngredient.Recipe.Id == recipeId); + + if (recipeToRemove != null) + { + _context.RecipeIngredientsShoppingLists.Remove(recipeToRemove); + await _context.SaveChangesAsync(); + } + } + } + } + + public async Task> GetShoppingListsByIngredientOfRecipeAsync(Guid ingredientId, Guid recipeId) + { + return await _context.ShoppingLists + .Where(sl => sl.RecipeIngredientShoppingLists.Any(risl => risl.RecipeIngredient.Ingredient.Id == ingredientId && risl.RecipeIngredient.Recipe.Id == recipeId)) + .ToListAsync(); + } + + public async Task GetRecipeByNameAndImageAsync(string recipeName, string imageFileName) + { + return await _context.Recipes + .Where(r => r.Name == recipeName && r.MediaFiles.Any(mf => mf.FileName == imageFileName)) + .Include(r => r.MediaFiles.Where(mf => mf.FileName == imageFileName)) + .FirstOrDefaultAsync(); + } + } +} diff --git a/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs b/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs new file mode 100644 index 0000000..a8244cd --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs @@ -0,0 +1,13 @@ +namespace Francesco.Recipes.World.Repositories.Unit +{ + using Francesco.Recipes.World.Models.BackendModels.Unit; + + public interface IUnitRepository + { + Task GetUnitByIdAsync(Guid unitId); + + Task AddUnitAsync(string name, string symbol); + + Task> GetAllUnitsAsync(); + } +} diff --git a/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs b/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs new file mode 100644 index 0000000..04297cd --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Unit/UnitRepository.cs @@ -0,0 +1,43 @@ +namespace Francesco.Recipes.World.Repositories.Unit +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.Unit; + using Microsoft.EntityFrameworkCore; + + public class UnitRepository : IUnitRepository + { + private readonly FrancescosRecipesWorldDbContext _context; + + public UnitRepository( + FrancescosRecipesWorldDbContext context) + { + _context = context; + } + + public async Task GetUnitByIdAsync(Guid unitId) + { + var unit = await _context.Units.FindAsync(unitId); + return unit ?? throw new InvalidDataException($"Address {unitId} not found."); + } + + public async Task AddUnitAsync(string name, string symbol) + { + var unit = new Unit + { + Id = Guid.NewGuid(), + Name = name, + Symbol = symbol, + }; + + _context.Units.Add(unit); + await _context.SaveChangesAsync(); + + return unit; + } + + public async Task> GetAllUnitsAsync() + { + return await _context.Units.ToListAsync(); + } + } +} From 1fcf1a1c26845c25e053147f388c6ed3dd8b3028 Mon Sep 17 00:00:00 2001 From: Francesco D'Amico Date: Tue, 1 Apr 2025 17:02:47 +0200 Subject: [PATCH 06/18] FilterByDifficulty testing --- .../Controller/Recipe/RecipeController.cs | 16 ++++------------ .../Models/BackendModels/Recipe/Difficulty.cs | 10 +++++----- .../Repositories/Recipe/IRecipeRepository.cs | 2 +- .../Repositories/Recipe/RecipeRepository.cs | 2 +- .../Views/Recipe/FilterByDifficulty.cshtml | 16 +++++++++++----- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 5b21475..9b4cd9e 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -5,7 +5,6 @@ using Francesco.Recipes.World.Repositories.Ingredient; using Francesco.Recipes.World.Repositories.Recipe; using Francesco.Recipes.World.Repositories.Unit; - using Francesco.Recipes.World.Views.Recipe; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; @@ -25,6 +24,8 @@ _ingredientRepository = ingredientRepository; } + public IReadOnlyCollection Recipes { get; set; } + // GET: /Recipe/AddOrCreateIngredient [HttpGet("{recipeId}/AddOrCreateIngredient")] public async Task AddOrCreateIngredient() @@ -140,17 +141,8 @@ [HttpGet("FilterByDifficulty")] public async Task FilterByDifficulty(Difficulty? difficulty) { - var viewModel = new FilterByDifficultyViewModel - { - SelectedDifficulty = difficulty, - }; - - if (difficulty.HasValue) - { - viewModel.Recipes = (await _recipeRepository.GetRecipesByDifficultyAsync(difficulty.Value)).ToList(); - } - - return View(viewModel); + Recipes = await _recipeRepository.GetRecipesByDifficultyAsync(difficulty); + return View(Recipes); } } } diff --git a/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs index 4456670..ee81fd2 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs @@ -2,14 +2,14 @@ { public enum Difficulty { - VeryEasy = 1, + VeryEasy = 0, - Easy = 2, + Easy = 1, - Medium = 3, + Medium = 2, - Hard = 4, + Hard = 3, - Expert = 5, + Expert = 4, } } diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs index 43a652d..bc6ae01 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -14,7 +14,7 @@ Task> GetRecipesByNameOrIngredientAsync(string name, string ingredient); - Task> GetRecipesByDifficultyAsync(Difficulty difficulty); + Task> GetRecipesByDifficultyAsync(Difficulty? difficulty); Task AddUnitToRecipeAsync(string name, string symbol); diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index 300c467..7b2c780 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -160,7 +160,7 @@ return await query.ToListAsync(); } - public async Task> GetRecipesByDifficultyAsync(Difficulty difficulty) + public async Task> GetRecipesByDifficultyAsync(Difficulty? difficulty) { return await _context.Recipes .Where(r => r.Difficulty == difficulty) diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml index 6b9ff3b..01a77cb 100644 --- a/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml @@ -7,13 +7,14 @@

Filter Recipes by Difficulty

-
+
- +
-
@if (Model.Recipes != null && Model.Recipes.Any()) @@ -22,7 +23,7 @@
    @foreach (var recipe in Model.Recipes) { -
  • @recipe.Name - @recipe.Difficulty
  • +
  • @recipe.Name - @recipe.Difficulty.
  • }
} @@ -32,5 +33,10 @@ else } @section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") - } +} From c7543f8a49c5677893488b03718bb6174aa61707 Mon Sep 17 00:00:00 2001 From: Francesco D'Amico Date: Wed, 2 Apr 2025 08:46:37 +0200 Subject: [PATCH 07/18] Edit Program and csproj file --- .../Francesco.Recipes.World.csproj | 5 +---- Francesco.Recipes.World/Program.cs | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Francesco.Recipes.World/Francesco.Recipes.World.csproj b/Francesco.Recipes.World/Francesco.Recipes.World.csproj index 728f575..549a91d 100644 --- a/Francesco.Recipes.World/Francesco.Recipes.World.csproj +++ b/Francesco.Recipes.World/Francesco.Recipes.World.csproj @@ -29,6 +29,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -41,10 +42,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index 6d773ec..719acda 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -1,8 +1,11 @@ using Francesco.Recipes.World.Data; - -using Francesco.Recipes.World.Repositories; - -using FrancescoRecipesWorld.Repositories; +using Francesco.Recipes.World.Repositories.Category; +using Francesco.Recipes.World.Repositories.Ingredient; +using Francesco.Recipes.World.Repositories.Instruction; +using Francesco.Recipes.World.Repositories.MediaFile; +using Francesco.Recipes.World.Repositories.Recipe; +using Francesco.Recipes.World.Repositories.ShoppingList; +using Francesco.Recipes.World.Repositories.Unit; using Microsoft.AspNetCore.Identity; @@ -34,6 +37,12 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); + +builder.Services.AddScoped(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -45,6 +54,8 @@ if (!app.Environment.IsDevelopment()) app.UseHsts(); } +Console.WriteLine("Standard Numeric Format Specifiers"); + app.UseHttpsRedirection(); app.UseStaticFiles(); From e438e8a114b06963f95e5eefa64dcbc423ec3717 Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 8 Apr 2025 14:25:21 +0200 Subject: [PATCH 08/18] implement FavoritRepository for managing favorite recipes --- .../Repositories/Favorit/FavoritRepository.cs | 49 +++++++++++++++++++ .../Favorit/IFavoritRepository.cs | 15 ++++++ 2 files changed, 64 insertions(+) create mode 100644 Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs create mode 100644 Francesco.Recipes.World/Repositories/Favorit/IFavoritRepository.cs diff --git a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs new file mode 100644 index 0000000..b4eddb5 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs @@ -0,0 +1,49 @@ +namespace Francesco.Recipes.World.Repositories.Favorit +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Microsoft.EntityFrameworkCore; + + public class FavoritRepository : IFavoritRepository + { + private readonly FrancescosRecipesWorldDbContext _context; + + public FavoritRepository(FrancescosRecipesWorldDbContext context) + { + _context = context; + } + + public async Task> GetFavoriteRecipesAsync() + { + return await _context.Recipes + .Where(r => r.IsFavorite) + .ToListAsync(); + } + + public async Task IsFavoriteAsync(Guid recipeId) + { + return await _context.Recipes + .AnyAsync(r => r.Id == recipeId && r.IsFavorite); + } + + public async Task AddFavoriteAsync(Guid recipeId) + { + var recipe = await _context.Recipes.FindAsync(recipeId); + if (recipe != null && !recipe.IsFavorite) + { + recipe.IsFavorite = true; + await _context.SaveChangesAsync(); + } + } + + public async Task RemoveFavoriteAsync(Guid recipeId) + { + var recipe = await _context.Recipes.FindAsync(recipeId); + if (recipe != null && recipe.IsFavorite) + { + recipe.IsFavorite = false; + await _context.SaveChangesAsync(); + } + } + } +} diff --git a/Francesco.Recipes.World/Repositories/Favorit/IFavoritRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/IFavoritRepository.cs new file mode 100644 index 0000000..8f47e0a --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Favorit/IFavoritRepository.cs @@ -0,0 +1,15 @@ +namespace Francesco.Recipes.World.Repositories.Favorit +{ + using Francesco.Recipes.World.Models.BackendModels.Recipe; + + public interface IFavoritRepository + { + Task> GetFavoriteRecipesAsync(); + + Task IsFavoriteAsync(Guid recipeId); + + Task AddFavoriteAsync(Guid recipeId); + + Task RemoveFavoriteAsync(Guid recipeId); + } +} From ffebc376e1321dc577c5cf7d77b01a3a8bef9b48 Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 8 Apr 2025 14:30:12 +0200 Subject: [PATCH 09/18] implement ingredient management methods --- .../Ingredient/IIngredientRepository.cs | 5 -- .../Ingredient/IngredientRepository.cs | 53 ++++++------------- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs b/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs index 013e2d1..702f174 100644 --- a/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs +++ b/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs @@ -1,21 +1,16 @@ namespace Francesco.Recipes.World.Repositories.Ingredient { using Francesco.Recipes.World.Models.BackendModels.Ingredient; - using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; public interface IIngredientRepository { - Task CreateIngredientToRecipeAsync(Recipe recipe, string ingredientName); - Task UpdateIngredientAsync(Ingredient ingredient); Task> GetIngredientsByRecipeIdAsync(Guid recipeId); Task> GetIngredientsByNameAsync(string name); - Task RemoveIngredientFromRecipeAsync(Recipe recipe, Guid ingredientId); - Task GetIngredientByIdAsync(Guid ingredientId); } } diff --git a/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs b/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs index f573a81..69a21ed 100644 --- a/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs +++ b/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs @@ -1,8 +1,8 @@ -namespace Francesco.Recipes.World.Repositories.Ingredient + +namespace Francesco.Recipes.World.Repositories.Ingredient { using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models.BackendModels.Ingredient; - using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; using Microsoft.EntityFrameworkCore; @@ -16,48 +16,29 @@ _context = context; } - public async Task CreateIngredientToRecipeAsync(Recipe recipe, string ingredientName) + public async Task UpdateRecipeIngredientAsync(RecipeIngredient recipeIngredient) { - if (recipe is null) + if (recipeIngredient == null) { - throw new ArgumentException("Recipe not found", nameof(recipe)); + throw new ArgumentNullException(nameof(recipeIngredient)); } - var newIngredient = new Ingredient - { - Id = Guid.NewGuid(), - Name = ingredientName, - }; + var existingRecipeIngredient = await _context.RecipeIngredients + .Include(ri => ri.Ingredient) + .Include(ri => ri.Unit) + .FirstOrDefaultAsync(ri => ri.Id == recipeIngredient.Id); - var recipeIngredient = new RecipeIngredient + if (existingRecipeIngredient == null) { - Id = Guid.NewGuid(), - Recipe = recipe, - Ingredient = newIngredient, - Quantity = 1, - }; + throw new InvalidOperationException($"RecipeIngredient with ID {recipeIngredient.Id} not found."); + } - _context.Ingredients.Add(newIngredient); - _context.RecipeIngredients.Add(recipeIngredient); + existingRecipeIngredient.Quantity = recipeIngredient.Quantity; + existingRecipeIngredient.Ingredient = recipeIngredient.Ingredient; + existingRecipeIngredient.Unit = recipeIngredient.Unit; + + _context.RecipeIngredients.Update(existingRecipeIngredient); await _context.SaveChangesAsync(); - - return newIngredient; - } - - public async Task RemoveIngredientFromRecipeAsync(Recipe recipe, Guid ingredientId) - { - if (recipe == null) - { - throw new ArgumentNullException(nameof(recipe)); - } - - var ingredientToRemove = recipe.RecipeIngredients.FirstOrDefault(i => i.Id == ingredientId); - - if (ingredientToRemove != null) - { - recipe.RecipeIngredients.Remove(ingredientToRemove); - await _context.SaveChangesAsync(); - } } public async Task> GetIngredientsByNameAsync(string name) From d4bcf7980bce8e8a3333a261e836721ae46d6d85 Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 8 Apr 2025 15:05:15 +0200 Subject: [PATCH 10/18] add instruction management for recipes --- .../Instruction/IInstructionRepository.cs | 7 +++ .../Instruction/InstructionRepository.cs | 59 ++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs index 7969570..1d22238 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs @@ -1,9 +1,16 @@ namespace Francesco.Recipes.World.Repositories.Instruction { using Francesco.Recipes.World.Models.BackendModels.Instruction; + using Francesco.Recipes.World.Models.BackendModels.Recipe; public interface IInstructionRepository { Task GetInstructionAsync(Guid instructionId); + + Task CreateInstructionToRecipeAsync(Guid recipeId, string description, int number); + + Task> GetInstructionsByRecipeIdAsync(Guid recipeId); + + Task RemoveInstructionFromRecipeAsync(Recipe recipe, Guid instructionId); } } diff --git a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs index b772813..09561c5 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs @@ -2,13 +2,18 @@ { using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models.BackendModels.Instruction; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Repositories.Recipe; + using Microsoft.EntityFrameworkCore; public class InstructionRepository : IInstructionRepository { private readonly FrancescosRecipesWorldDbContext _context; + private readonly IRecipeRepository _recipeRepository; - public InstructionRepository(FrancescosRecipesWorldDbContext context) + public InstructionRepository(FrancescosRecipesWorldDbContext context, IRecipeRepository recipeRepository) { + _recipeRepository = recipeRepository; _context = context; } @@ -17,5 +22,57 @@ var instruction = await _context.Instructions.FindAsync(instructionId); return instruction ?? throw new InvalidDataException($"Instruction {instructionId} not found."); } + + public async Task CreateInstructionToRecipeAsync(Guid recipeId, string description, int number) + { + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + + if (string.IsNullOrWhiteSpace(description)) + { + throw new ArgumentException("Description cannot be empty", nameof(description)); + } + + if (number <= 0) + { + throw new ArgumentOutOfRangeException(nameof(number), "Number must be greater than 0."); + } + + var newInstruction = new Instruction + { + Id = Guid.NewGuid(), + Description = description, + Number = number, + Recipe = recipe, + }; + + _context.Instructions.Add(newInstruction); + await _context.SaveChangesAsync(); + + return newInstruction; + } + + public async Task RemoveInstructionFromRecipeAsync(Recipe recipe, Guid instructionId) + { + if (recipe == null) + { + throw new ArgumentNullException(nameof(recipe)); + } + + var instructionToRemove = recipe.Instructions.FirstOrDefault(i => i.Id == instructionId); + + if (instructionToRemove != null) + { + recipe.Instructions.Remove(instructionToRemove); + await _context.SaveChangesAsync(); + } + } + + public async Task> GetInstructionsByRecipeIdAsync(Guid recipeId) + { + return await _context.Instructions + .Include(i => i.Recipe) + .Where(i => i.Recipe.Id == recipeId) + .ToListAsync(); + } } } From 31fbafe806d607dd7098d10cc4ec6e0a2b0d7104 Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 8 Apr 2025 15:05:49 +0200 Subject: [PATCH 11/18] add repository logic for mediafile --- .../MediaFile/IMediaFileRepository.cs | 6 +- .../MediaFile/MediaFileRepository.cs | 97 +++++++++++++------ 2 files changed, 70 insertions(+), 33 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs index 0fc5d91..879a1b2 100644 --- a/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs +++ b/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs @@ -2,12 +2,10 @@ { public interface IMediaFileRepository { - Task ReplaceInstructionImageAsync(Guid instructionId, IFormFile? newPhoto); + Task ReplaceInstructionImageAsync(Guid instructionId, Guid mediafileId, IFormFile? newPhoto); Task UploadInstructionImageAsync(Guid instructionId, IFormFile? photo); - Task ReplaceRecipeImageAsync(Guid recipeId, IFormFile? newPhoto); - - Task UploadRecipeImageAsync(Guid recipeId, IFormFile? photo); + Task UploadRecipeMediaAsync(Guid recipeId, IFormFile mediaFile); } } diff --git a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs index 742e4c4..b4beb26 100644 --- a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -18,7 +18,7 @@ _recipeRepository = recipeRepository; } - public async Task ReplaceInstructionImageAsync(Guid instructionId, IFormFile? newPhoto) + public async Task ReplaceInstructionImageAsync(Guid instructionId, Guid mediaFileIdToReplace, IFormFile? newPhoto) { if (newPhoto is null) { @@ -26,24 +26,32 @@ } var instruction = await _instructionRepository.GetInstructionAsync(instructionId); - _context.RemoveRange(instruction.MediaFiles); - await _context.SaveChangesAsync(); - await UploadInstructionImageAsync(instructionId, newPhoto); - } - - public async Task ReplaceRecipeImageAsync(Guid recipeId, IFormFile? newPhoto) - { - if (newPhoto is null) + var mediaToReplace = instruction.MediaFiles.FirstOrDefault(m => m.Id == mediaFileIdToReplace); + if (mediaToReplace == null) { - throw new ArgumentNullException(nameof(newPhoto)); + throw new InvalidOperationException("The specified media file does not exist."); } - var recipe = await _recipeRepository.GetRecipeAsync(recipeId); - _context.RemoveRange(recipe.MediaFiles); + _context.MediaFiles.Remove(mediaToReplace); await _context.SaveChangesAsync(); - await UploadInstructionImageAsync(recipeId, newPhoto); + using (var memoryStream = new MemoryStream()) + { + await newPhoto.CopyToAsync(memoryStream); + + var newMedia = new MediaFile + { + Id = Guid.NewGuid(), + FileName = newPhoto.FileName, + MimeType = newPhoto.ContentType, + Data = memoryStream.ToArray(), + Instruction = instruction, + }; + + _context.MediaFiles.Add(newMedia); + await _context.SaveChangesAsync(); + } } public async Task UploadInstructionImageAsync(Guid instructionId, IFormFile? photo) @@ -72,30 +80,61 @@ } } - public async Task UploadRecipeImageAsync(Guid recipeId, IFormFile? photo) + public async Task UploadRecipeMediaAsync(Guid recipeId, IFormFile mediaFile) { - if (photo is null) + if (mediaFile == null) { - throw new ArgumentNullException(nameof(photo)); + throw new ArgumentNullException(nameof(mediaFile)); } var recipe = await _recipeRepository.GetRecipeAsync(recipeId); - using (var memoryStream = new MemoryStream()) + if (recipe == null) { - await photo.CopyToAsync(memoryStream); - - var recipeImage = new MediaFile - { - FileName = photo.FileName, - MimeType = photo.ContentType, - Data = memoryStream.ToArray(), - Recipe = recipe, - }; - - _context.Add(recipeImage); - await _context.SaveChangesAsync(); + throw new InvalidOperationException("The specified recipe does not exist."); } + + var isImage = mediaFile.ContentType.StartsWith("image/"); + var isVideo = mediaFile.ContentType.StartsWith("video/"); + + if (!isImage && !isVideo) + { + throw new InvalidOperationException("Only image or video files are allowed."); + } + + if (isImage) + { + var existingImage = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType?.StartsWith("image/") == true); + if (existingImage != null) + { + _context.MediaFiles.Remove(existingImage); + await _context.SaveChangesAsync(); + } + } + else if (isVideo) + { + var existingVideo = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType?.StartsWith("video/") == true); + if (existingVideo != null) + { + _context.MediaFiles.Remove(existingVideo); + await _context.SaveChangesAsync(); + } + } + + using var memoryStream = new MemoryStream(); + await mediaFile.CopyToAsync(memoryStream); + + var newMedia = new MediaFile + { + Id = Guid.NewGuid(), + FileName = mediaFile.FileName, + MimeType = mediaFile.ContentType, + Data = memoryStream.ToArray(), + Recipe = recipe, + }; + + _context.MediaFiles.Add(newMedia); + await _context.SaveChangesAsync(); } } } From ddd0cad36b7b033f3283441816affbd29f309c97 Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 8 Apr 2025 15:06:23 +0200 Subject: [PATCH 12/18] add recipe managment --- .../Repositories/Recipe/IRecipeRepository.cs | 5 +- .../Repositories/Recipe/RecipeRepository.cs | 47 +++++++++++++------ 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs index bc6ae01..c5d46a0 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -2,12 +2,13 @@ { using Francesco.Recipes.World.Models.BackendModels.Category; using Francesco.Recipes.World.Models.BackendModels.Recipe; - using Francesco.Recipes.World.Models.BackendModels.Unit; public interface IRecipeRepository { Task GetRecipeAsync(Guid recipeId); + Task GetRecipeByIdAsync(Guid id); + Task AddOrCreateIngredientToRecipeAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId); Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId); @@ -16,8 +17,6 @@ Task> GetRecipesByDifficultyAsync(Difficulty? difficulty); - Task AddUnitToRecipeAsync(string name, string symbol); - Task CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime); } } diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index 7b2c780..ae33b43 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -5,7 +5,6 @@ using Francesco.Recipes.World.Models.BackendModels.Ingredient; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; - using Francesco.Recipes.World.Models.BackendModels.Unit; using Francesco.Recipes.World.Repositories.Ingredient; using Francesco.Recipes.World.Repositories.Unit; using Microsoft.EntityFrameworkCore; @@ -30,6 +29,17 @@ return recipe ?? throw new InvalidDataException($"Address {recipeId} not found."); } + public async Task GetRecipeByIdAsync(Guid recipeId) + { + return await _context.Recipes + .Include(r => r.RecipeIngredients) + .ThenInclude(ri => ri.Ingredient) + .Include(r => r.RecipeIngredients) + .ThenInclude(ri => ri.Unit) + .Include(r => r.MediaFiles) + .FirstOrDefaultAsync(r => r.Id == recipeId); + } + public async Task AddOrCreateIngredientToRecipeAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId) { var recipe = await GetRecipeAsync(recipeId); @@ -77,13 +87,13 @@ } public async Task CreateRecipeForCategoryAsync( - Category category, - string name, - string description, - Difficulty difficulty, - int servings, - TimeSpan preparationTime, - TimeSpan cookingTime) + Category category, + string name, + string description, + Difficulty difficulty, + int servings, + TimeSpan preparationTime, + TimeSpan cookingTime) { if (category == null) { @@ -162,14 +172,21 @@ public async Task> GetRecipesByDifficultyAsync(Difficulty? difficulty) { - return await _context.Recipes - .Where(r => r.Difficulty == difficulty) - .ToListAsync(); - } + if (!difficulty.HasValue) + { + return await _context.Recipes + .Include(r => r.Category) + .Include(r => r.RecipeIngredients) + .ThenInclude(ri => ri.Ingredient) + .ToListAsync(); + } - public async Task AddUnitToRecipeAsync(string name, string symbol) - { - return await _unitRepository.AddUnitAsync(name, symbol); + return await _context.Recipes + .Where(r => r.Difficulty == difficulty.Value) + .Include(r => r.Category) + .Include(r => r.RecipeIngredients) + .ThenInclude(ri => ri.Ingredient) + .ToListAsync(); } } } From 712b561d12da1c1f745507229f94ff7102bd2ddf Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 8 Apr 2025 15:06:50 +0200 Subject: [PATCH 13/18] add shoppinglist managment for ingredient --- .../ShoppingList/IShoppingListRepository.cs | 15 +- .../ShoppingList/ShoppingListRepository.cs | 177 ++++++++++++------ 2 files changed, 132 insertions(+), 60 deletions(-) diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs index 2205dbc..5411e89 100644 --- a/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs +++ b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs @@ -1,18 +1,19 @@ - -namespace Francesco.Recipes.World.Repositories.ShoppingList +namespace Francesco.Recipes.World.Repositories.ShoppingList { + using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; using Francesco.Recipes.World.Models.BackendModels.Recipe; - using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; public interface IShoppingListRepository { - Task AddIngredientToShoppingListAsync(Guid recipeIngredientId, Guid shoppingListId); + Task AddIngredientsToShoppingListAsync(Guid shoppingListId, List ingredientIds); - Task RemoveRecipeIngredientFromShoppingListAsync(Guid recipeIngredientId); + Task> GetIngredientsOfRecipeInListAsync(Guid shoppingListRecipeId); - Task RemoveRecipeFromShoppingListIfEmptyAsync(Guid recipeId, Guid shoppingListId); + Task UpdateIngredientCheckedAsync(Guid shoppingListRecipeId, Guid recipeIngredientId, bool isChecked); - Task> GetShoppingListsByIngredientOfRecipeAsync(Guid ingredientId, Guid recipeId); + Task RemoveRecipeIfEmptyAsync(Guid shoppingListRecipeId); + + Task DeleteShoppingListAsync(Guid shoppingListId); Task GetRecipeByNameAndImageAsync(string recipeName, string imageFileName); } diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs index 5609143..45793f0 100644 --- a/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs +++ b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs @@ -3,6 +3,7 @@ using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; using Microsoft.EntityFrameworkCore; @@ -15,92 +16,162 @@ _context = context; } - public async Task AddIngredientToShoppingListAsync(Guid recipeIngredientId, Guid shoppingListId) + public async Task AddIngredientsToShoppingListAsync(Guid recipeId, List ingredientIds) { - var existingEntry = await _context.RecipeIngredientsShoppingLists - .FirstOrDefaultAsync(risl => risl.RecipeIngredient.Id == recipeIngredientId && risl.ShoppingList.Id == shoppingListId); - - if (existingEntry != null) + if (ingredientIds == null || !ingredientIds.Any()) { - return; + throw new ArgumentNullException(nameof(ingredientIds)); } - var recipeIngredient = await _context.RecipeIngredients - .FirstOrDefaultAsync(ri => ri.Id == recipeIngredientId); + var recipe = await _context.Recipes + .Include(r => r.RecipeIngredients) + .FirstOrDefaultAsync(r => r.Id == recipeId); - if (recipeIngredient == null) + if (recipe == null) { - throw new InvalidOperationException($"Recipe ingredient with ID {recipeIngredientId} not found."); + throw new Exception("Rezept nicht gefunden."); } var shoppingList = await _context.ShoppingLists - .FirstOrDefaultAsync(sl => sl.Id == shoppingListId); + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.SelectedIngredients) + .ThenInclude(si => si.RecipeIngredient) + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.Recipe) + .FirstOrDefaultAsync(sl => sl.RecipeShoppingList.Any(rsl => rsl.Recipe.Id == recipeId)); + + var recipeIngredients = await _context.RecipeIngredients + .Where(ri => ingredientIds.Contains(ri.Id) && ri.Recipe.Id == recipeId) + .ToListAsync(); + + if (!recipeIngredients.Any()) + { + throw new Exception("Keine gültigen Zutaten gefunden."); + } if (shoppingList == null) { - throw new InvalidOperationException($"Shopping list with ID {shoppingListId} not found."); + shoppingList = new ShoppingList + { + Id = Guid.NewGuid(), + CreatedAt = DateTime.UtcNow, + ModifiedAt = null, + RecipeShoppingList = new List(), + }; + + var newRecipeList = new RecipeShoppingList + { + Id = Guid.NewGuid(), + Recipe = recipe, + SelectedIngredients = recipeIngredients.Select(ri => new RecipeIngredientShoppingList + { + Id = Guid.NewGuid(), + RecipeIngredient = ri, + IsChecked = false, + }).ToList(), + }; + + shoppingList.RecipeShoppingList.Add(newRecipeList); + _context.ShoppingLists.Add(shoppingList); + } + else + { + var existingRecipeList = shoppingList.RecipeShoppingList + .FirstOrDefault(rsl => rsl.Recipe.Id == recipeId); + + if (existingRecipeList == null) + { + existingRecipeList = new RecipeShoppingList + { + Id = Guid.NewGuid(), + Recipe = recipe, + SelectedIngredients = new List(), + }; + + shoppingList.RecipeShoppingList.Add(existingRecipeList); + } + + foreach (var ri in recipeIngredients) + { + var alreadyExists = existingRecipeList.SelectedIngredients + .Any(si => si.RecipeIngredient.Id == ri.Id); + + if (!alreadyExists) + { + existingRecipeList.SelectedIngredients.Add(new RecipeIngredientShoppingList + { + Id = Guid.NewGuid(), + RecipeIngredient = ri, + IsChecked = false, + }); + } + } + + shoppingList.ModifiedAt = DateTime.UtcNow; + } + } + + public async Task> GetIngredientsOfRecipeInListAsync(Guid shoppingListRecipeId) + { + return await _context.RecipeIngredientsShoppingLists + .AsNoTracking() + .Include(i => i.RecipeIngredient) + .ThenInclude(ri => ri.Ingredient) + .Include(i => i.RecipeIngredient.Unit) + .Where(i => i.RecipeShoppingList.Id == shoppingListRecipeId) + .ToListAsync(); + } + + public async Task UpdateIngredientCheckedAsync(Guid shoppingListRecipeId, Guid recipeIngredientId, bool isChecked) + { + var item = await _context.RecipeIngredientsShoppingLists + .FirstOrDefaultAsync(i => + i.RecipeShoppingList.Id == shoppingListRecipeId && + i.RecipeIngredient.Id == recipeIngredientId); + + if (item == null) + { + throw new Exception("Zutat nicht gefunden."); } - var newEntry = new RecipeIngredientShoppingList - { - RecipeIngredient = recipeIngredient, - ShoppingList = shoppingList, - }; - - _context.RecipeIngredientsShoppingLists.Add(newEntry); + item.IsChecked = isChecked; await _context.SaveChangesAsync(); } - public async Task RemoveRecipeIngredientFromShoppingListAsync(Guid recipeIngredientId) + public async Task RemoveRecipeIfEmptyAsync(Guid shoppingListRecipeId) { - var entry = await _context.RecipeIngredientsShoppingLists - .FirstOrDefaultAsync(risl => risl.RecipeIngredient.Id == recipeIngredientId); + var recipeEntry = await _context.RecipeShoppingLists + .Include(r => r.SelectedIngredients) + .FirstOrDefaultAsync(r => r.Id == shoppingListRecipeId); - if (entry != null) + if (recipeEntry != null && !recipeEntry.SelectedIngredients.Any()) { - _context.RecipeIngredientsShoppingLists.Remove(entry); + _context.RecipeShoppingLists.Remove(recipeEntry); await _context.SaveChangesAsync(); } } - public async Task RemoveRecipeFromShoppingListIfEmptyAsync(Guid recipeId, Guid shoppingListId) + public async Task DeleteShoppingListAsync(Guid shoppingListId) { - var hasIngredients = await _context.RecipeIngredientsShoppingLists - .AnyAsync(risl => risl.RecipeIngredient.Recipe.Id == recipeId && risl.ShoppingList.Id == shoppingListId); + var list = await _context.ShoppingLists + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(r => r.SelectedIngredients) + .FirstOrDefaultAsync(sl => sl.Id == shoppingListId); - if (!hasIngredients) + if (list != null) { - var shoppingList = await _context.ShoppingLists - .Include(sl => sl.RecipeIngredientShoppingLists) - .FirstOrDefaultAsync(sl => sl.Id == shoppingListId); - - if (shoppingList != null) - { - var recipeToRemove = shoppingList.RecipeIngredientShoppingLists - .FirstOrDefault(risl => risl.RecipeIngredient.Recipe.Id == recipeId); - - if (recipeToRemove != null) - { - _context.RecipeIngredientsShoppingLists.Remove(recipeToRemove); - await _context.SaveChangesAsync(); - } - } + _context.ShoppingLists.Remove(list); + await _context.SaveChangesAsync(); } } - public async Task> GetShoppingListsByIngredientOfRecipeAsync(Guid ingredientId, Guid recipeId) - { - return await _context.ShoppingLists - .Where(sl => sl.RecipeIngredientShoppingLists.Any(risl => risl.RecipeIngredient.Ingredient.Id == ingredientId && risl.RecipeIngredient.Recipe.Id == recipeId)) - .ToListAsync(); - } - public async Task GetRecipeByNameAndImageAsync(string recipeName, string imageFileName) { return await _context.Recipes - .Where(r => r.Name == recipeName && r.MediaFiles.Any(mf => mf.FileName == imageFileName)) - .Include(r => r.MediaFiles.Where(mf => mf.FileName == imageFileName)) - .FirstOrDefaultAsync(); + .AsNoTracking() + .Where(r => r.Name == recipeName && r.MediaFiles.Any(mf => mf.FileName == imageFileName)) + .Include(r => r.MediaFiles.Where(mf => mf.FileName == imageFileName)) + .FirstOrDefaultAsync(); } } } From 36bca4d6584d644a81c2fd755d53ae6bea018419 Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 8 Apr 2025 15:22:28 +0200 Subject: [PATCH 14/18] add CategoryController with endpoints --- .../Controller/Category/CategoryController.cs | 3 +- .../MediaFile/MediaFileController.cs | 44 +- .../Controller/Recipe/RecipeController.cs | 160 ++++- .../ShoppingList/ShoppingListController.cs | 48 +- .../Data/FrancescosRecipesWorldDbContext.cs | 5 +- .../Francesco.Recipes.World.csproj | 10 +- ...120842_UpdateShoppingLIstLogic.Designer.cs | 572 ++++++++++++++++++ .../20250402120842_UpdateShoppingLIstLogic.cs | 162 +++++ ...escosRecipesWorldDbContextModelSnapshot.cs | 73 ++- .../Models/BackendModels/Recipe/Difficulty.cs | 9 +- .../RecipeIngredientShoppingList.cs | 6 +- .../RecipeShoppingList/RecipeShoppingList.cs | 17 + .../Shoppinglist/ShoppingList.cs | 4 +- Francesco.Recipes.World/Program.cs | 12 +- .../Category/CategoryRecipesViewModel.cs | 12 + .../Views/Recipe/AddInstruction.cshtml | 59 ++ .../Views/Recipe/AddOrCreateIngredient.cshtml | 36 +- .../Views/Recipe/CategoryRecipes.cshtml | 98 +++ .../Views/Recipe/Create.cshtml | 78 +-- .../Views/Recipe/Details.cshtml | 68 +++ .../Views/Recipe/FilterByDifficulty.cshtml | 100 +-- .../Recipe/FilterByDifficultyViewModel.cs | 2 +- ...AddIngredientsToShoppingListPartial.cshtml | 43 ++ .../Views/ShoppingList/Details.cshtml | 56 ++ 24 files changed, 1535 insertions(+), 142 deletions(-) create mode 100644 Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs create mode 100644 Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs create mode 100644 Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs create mode 100644 Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs create mode 100644 Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml create mode 100644 Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml create mode 100644 Francesco.Recipes.World/Views/Recipe/Details.cshtml create mode 100644 Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml create mode 100644 Francesco.Recipes.World/Views/ShoppingList/Details.cshtml diff --git a/Francesco.Recipes.World/Controller/Category/CategoryController.cs b/Francesco.Recipes.World/Controller/Category/CategoryController.cs index 0257ab4..29b06b5 100644 --- a/Francesco.Recipes.World/Controller/Category/CategoryController.cs +++ b/Francesco.Recipes.World/Controller/Category/CategoryController.cs @@ -5,6 +5,7 @@ using Francesco.Recipes.World.Repositories.Category; using Microsoft.AspNetCore.Mvc; + [ValidateAntiForgeryToken] [Route("Category")] public class CategoryController : Controller @@ -37,7 +38,7 @@ public async Task>> GetRecipesByCategory(Guid id) { var recipes = await _categoryRepository.GetRecipesByCategoryAsync(id); - return Ok(recipes); + return View(recipes); } } } diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs index 6cec16d..14f59fe 100644 --- a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -1,6 +1,48 @@ namespace Francesco.Recipes.World.Controller.MediaFile { - public class MediaFileController + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Repositories.MediaFile; + using Microsoft.AspNetCore.Mvc; + + [ValidateAntiForgeryToken] + + [Route("categories/{categoryId}/Recipe")] + public class MediaFileController : Controller { + private readonly IMediaFileRepository _mediaFileRepository; + private readonly FrancescosRecipesWorldDbContext _context; + + public MediaFileController(IMediaFileRepository mediaFileRepository, FrancescosRecipesWorldDbContext context) + { + _mediaFileRepository = mediaFileRepository; + _context = context; + } + + // POST: /UploadImage + [HttpPost("UploadImage")] + public async Task UploadImage(Guid recipeId, IFormFile? mediaFile) + { + if (mediaFile is null) + { + return BadRequest("Photo is required."); + } + + try + { + await _mediaFileRepository.UploadRecipeMediaAsync(recipeId, mediaFile); + return Ok("Image uploaded successfully."); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } + + // GET: /UploadImage + [HttpGet("UploadImage")] + public IActionResult UploadImageView() + { + return View(); + } } } diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 9b4cd9e..05d99aa 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -1,43 +1,94 @@ namespace Francesco.Recipes.World.Controller.Recipe { + using System.ComponentModel.DataAnnotations; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Repositories.Category; + using Francesco.Recipes.World.Repositories.Favorit; using Francesco.Recipes.World.Repositories.Ingredient; + using Francesco.Recipes.World.Repositories.Instruction; + using Francesco.Recipes.World.Repositories.MediaFile; using Francesco.Recipes.World.Repositories.Recipe; using Francesco.Recipes.World.Repositories.Unit; + using Francesco.Recipes.World.Views.Category; + using Francesco.Recipes.World.Views.Recipe; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; - [Route("categories/{categoryId}/Recipe")] + [ValidateAntiForgeryToken] + [Route("Recipe")] public class RecipeController : Controller { private readonly IRecipeRepository _recipeRepository; private readonly IUnitRepository _unitRepository; private readonly ICategoryRepository _categoryRepository; private readonly IIngredientRepository _ingredientRepository; + private readonly IMediaFileRepository _mediaFileRepository; + private readonly IInstructionRepository _instructionRepository; + private readonly IFavoritRepository _favoritRepository; - public RecipeController(IRecipeRepository recipeRepository, IUnitRepository unitRepository, ICategoryRepository categoryRepository, IIngredientRepository ingredientRepository) + [Display(Name = "Schwierigkeitsgrad")] + [BindProperty(SupportsGet = true)] + public Difficulty? SelectedDifficulty { get; set; } + + public RecipeController(IRecipeRepository recipeRepository, IUnitRepository unitRepository, ICategoryRepository categoryRepository, IIngredientRepository ingredientRepository, IMediaFileRepository mediaFileRepository, IInstructionRepository instructionRepository, IFavoritRepository favoritRepository) { _recipeRepository = recipeRepository; _unitRepository = unitRepository; _categoryRepository = categoryRepository; _ingredientRepository = ingredientRepository; + Recipes = new List(); + _mediaFileRepository = mediaFileRepository; + _instructionRepository = instructionRepository; + _favoritRepository = favoritRepository; } public IReadOnlyCollection Recipes { get; set; } - // GET: /Recipe/AddOrCreateIngredient + // GET: /Recipe/{recipeId}/AddOrCreateIngredient [HttpGet("{recipeId}/AddOrCreateIngredient")] - public async Task AddOrCreateIngredient() + public async Task AddOrCreateIngredient(Guid recipeId) { var units = await _unitRepository.GetAllUnitsAsync(); ViewBag.Units = new SelectList(units, "Id", "Name"); + ViewBag.RecipeId = recipeId; return View(); } - // POST: /Recipe/AddOrCreateIngredient + // GET: /Recipe/Details/{recipeId} + [HttpGet("Details/{recipeId}")] + public async Task Details(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return View(recipe); + } + + // GET: /Recipe/CategoryRecipes + [HttpGet("CategoryRecipes")] + public async Task CategoryRecipes() + { + var categories = await _categoryRepository.GetAllCategoriesAsync(); + var viewModel = new List(); + + foreach (var category in categories) + { + var recipes = await _categoryRepository.GetRecipesByCategoryAsync(category.Id); + viewModel.Add(new CategoryRecipesViewModel + { + Category = category, + Recipes = recipes, + }); + } + + return View(viewModel); + } + + // POST: /Recipe/{recipeId}/AddOrCreateIngredient [HttpPost("{recipeId}/AddOrCreateIngredient")] - [ValidateAntiForgeryToken] public async Task AddOrCreateIngredient(Guid recipeId, string ingredientName, int quantity, Guid unitId) { if (quantity <= 0) @@ -56,8 +107,8 @@ return RedirectToAction("Details", new { id = recipeId }); } - // GET: /categories/{categoryId}/Recipe/Create - [HttpGet("Create")] + // GET: /Recipe/Create/{categoryId} + [HttpGet("Create/{categoryId}")] public async Task Create(Guid categoryId) { var category = await _categoryRepository.GetCategoryByIdAsync(categoryId); @@ -70,10 +121,9 @@ return View(); } - // POST: /categories/{categoryId}/Recipe/Create - [HttpPost("Create")] - [ValidateAntiForgeryToken] - public async Task Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime) + // POST: /Recipe/Create/{categoryId} + [HttpPost("Create/{categoryId}")] + public async Task Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime, IFormFile? photo) { if (string.IsNullOrWhiteSpace(name)) { @@ -103,13 +153,18 @@ return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } - await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); + var recipe = await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); + if (photo != null) + { + await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, photo); + } + return RedirectToAction("Details", "Category", new { id = categoryId }); } - // GET: /categories/{categoryId}/Recipe/{recipeId}/RemoveIngredient/{ingredientId} + // GET: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} [HttpGet("{recipeId}/RemoveIngredient/{ingredientId}")] - public async Task RemoveIngredient(Guid categoryId, Guid recipeId, Guid ingredientId) + public async Task RemoveIngredient(Guid recipeId, Guid ingredientId) { var recipe = await _recipeRepository.GetRecipeAsync(recipeId); var ingredient = await _ingredientRepository.GetIngredientByIdAsync(ingredientId); @@ -121,28 +176,87 @@ ViewBag.RecipeId = recipeId; ViewBag.IngredientId = ingredientId; - ViewBag.CategoryId = categoryId; ViewBag.IngredientName = ingredient.Name; return View(); } - // POST: /categories/{categoryId}/Recipe/{recipeId}/RemoveIngredient/{ingredientId} + // POST: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} [HttpPost("{recipeId}/RemoveIngredient/{ingredientId}")] - [ValidateAntiForgeryToken] - public async Task RemoveIngredientConfirmed(Guid categoryId, Guid recipeId, Guid ingredientId) + public async Task RemoveIngredientConfirmed(Guid recipeId, Guid ingredientId) { await _recipeRepository.RemoveIngredientFromRecipeAsync(recipeId, ingredientId); TempData["SuccessMessage"] = "Ingredient removed successfully."; return RedirectToAction("Details", new { id = recipeId }); } - // GET: /categories/{categoryId}/Recipe/FilterByDifficulty + // GET: /Recipe/FilterByDifficulty [HttpGet("FilterByDifficulty")] - public async Task FilterByDifficulty(Difficulty? difficulty) + public async Task FilterByDifficulty(Difficulty? selectedDifficulty) { - Recipes = await _recipeRepository.GetRecipesByDifficultyAsync(difficulty); - return View(Recipes); + var recipes = await _recipeRepository.GetRecipesByDifficultyAsync(selectedDifficulty); + var viewModel = new FilterByDifficultyViewModel + { + SelectedDifficulty = selectedDifficulty, + Recipes = recipes, + }; + return View(viewModel); + } + + // GET: /Recipe/{recipeId}/AddInstruction + [HttpGet("{recipeId}/AddInstruction")] + public async Task AddInstruction(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + ViewBag.RecipeId = recipeId; + return View(recipe); + } + + // POST: /Recipe/{recipeId}/AddInstruction + [HttpPost("{recipeId}/AddInstruction")] + public async Task AddInstruction(Guid recipeId, string description, int number) + { + try + { + await _instructionRepository.CreateInstructionToRecipeAsync(recipeId, description, number); + return RedirectToAction("AddInstruction", new { recipeId }); + } + catch (Exception ex) + { + ModelState.AddModelError(string.Empty, ex.Message); + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + ViewBag.RecipeId = recipeId; + return View(recipe); + } + } + + // GET: /Recipe/Favorites + [HttpGet("Favorites")] + public async Task Favorites() + { + var favoriteRecipes = await _favoritRepository.GetFavoriteRecipesAsync(); + return View(favoriteRecipes); + } + + // POST: /Recipe/AddFavorite + [HttpPost("AddFavorite")] + public async Task AddFavorite(Guid recipeId) + { + await _favoritRepository.AddFavoriteAsync(recipeId); + return RedirectToAction("Details", new { recipeId }); + } + + // POST: /Recipe/RemoveFavorite + [HttpPost("RemoveFavorite")] + public async Task RemoveFavorite(Guid recipeId) + { + await _favoritRepository.RemoveFavoriteAsync(recipeId); + return RedirectToAction("Details", new { recipeId }); } } } diff --git a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs index 38fdf4b..7f6a434 100644 --- a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs +++ b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs @@ -1,6 +1,50 @@ -namespace Francesco.Recipes.World.Controller.ShoppingList +namespace Francesco.Recipes.World.Controllers { - public class ShoppingListController + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Repositories.ShoppingList; + using Microsoft.AspNetCore.Mvc; + using Microsoft.EntityFrameworkCore; + + [Route("ShoppingList")] + public class ShoppingListController : Controller { + private readonly IShoppingListRepository _shoppingListRepository; + private readonly FrancescosRecipesWorldDbContext _context; + + public ShoppingListController(IShoppingListRepository shoppingListRepository, FrancescosRecipesWorldDbContext context) + { + _shoppingListRepository = shoppingListRepository; + _context = context; + } + + [HttpPost("CreateOrAddIngredients")] + public async Task CreateOrAddIngredients([FromBody] CreateOrAddIngredientsRequest request) + { + if (request == null || request.IngredientIds == null || !request.IngredientIds.Any()) + { + return BadRequest("No ingredients provided."); + } + + await _shoppingListRepository.AddIngredientsToShoppingListAsync(request.RecipeId, request.IngredientIds); + + var shoppingList = await _context.ShoppingLists + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.Recipe) + .FirstOrDefaultAsync(sl => sl.RecipeShoppingList.Any(rsl => rsl.Recipe.Id == request.RecipeId)); + + if (shoppingList == null) + { + return BadRequest("Error creating shopping list."); + } + + return Json(new { shoppingListId = shoppingList.Id }); + } + + public class CreateOrAddIngredientsRequest + { + public Guid RecipeId { get; set; } + + public List IngredientIds { get; set; } = new (); + } } } diff --git a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs index 4fd6bd8..7a8a5e7 100644 --- a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs +++ b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs @@ -9,7 +9,8 @@ using Francesco.Recipes.World.Models.BackendModels.MediaFile; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; - using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; +using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; +using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; using Francesco.Recipes.World.Models.BackendModels.Unit; using Microsoft.EntityFrameworkCore; @@ -36,6 +37,8 @@ public DbSet RecipeIngredientsShoppingLists => Set(); + public DbSet RecipeShoppingLists => Set(); + public DbSet ShoppingLists => Set(); public DbSet MediaFiles => Set(); diff --git a/Francesco.Recipes.World/Francesco.Recipes.World.csproj b/Francesco.Recipes.World/Francesco.Recipes.World.csproj index 549a91d..7d430a2 100644 --- a/Francesco.Recipes.World/Francesco.Recipes.World.csproj +++ b/Francesco.Recipes.World/Francesco.Recipes.World.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -45,5 +45,13 @@ + + + + + + + + diff --git a/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs new file mode 100644 index 0000000..d99ae26 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs @@ -0,0 +1,572 @@ +// + +using Francesco.Recipes.World.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + [DbContext(typeof(FrancescosRecipesWorldDbContext))] + [Migration("20250402120842_UpdateShoppingLIstLogic")] + partial class UpdateShoppingLIstLogic + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"), + Name = "Vorspeisen & Snacks" + }, + new + { + Id = new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), + Name = "Erste Gänge" + }, + new + { + Id = new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), + Name = "Hauptgerichte" + }, + new + { + Id = new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"), + Name = "Desserts & Süßspeisen" + }, + new + { + Id = new Guid("28e39168-701a-4084-81da-d96c987c462f"), + Name = "Beilagen & Salate" + }, + new + { + Id = new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"), + Name = "Kuchen" + }, + new + { + Id = new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), + Name = "Hefegebäck & Brot" + }, + new + { + Id = new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"), + Name = "Soßen & Saucen" + }, + new + { + Id = new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), + Name = "Marmeladen & Eingemachtes" + }, + new + { + Id = new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), + Name = "Getränke" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Favorits"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsChecked") + .HasColumnType("bit"); + + b.Property("RecipeIngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeIngredientId"); + + b.HasIndex("RecipeShoppingListId"); + + b.ToTable("RecipeIngredientsShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.ToTable("Instructions"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .HasColumnType("varbinary(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("InstructionId") + .HasColumnType("uniqueidentifier"); + + b.Property("MimeType") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("InstructionId"); + + b.HasIndex("RecipeId"); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CookingTime") + .HasColumnType("time"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Difficulty") + .HasColumnType("int"); + + b.Property("FavoritId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsFavorite") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PreparationTime") + .HasColumnType("time"); + + b.Property("Servings") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("FavoritId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UnitId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeId"); + + b.HasIndex("UnitId"); + + b.ToTable("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.HasIndex("ShoppingListId"); + + b.ToTable("RecipeShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("ShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Units"); + + b.HasData( + new + { + Id = new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), + Name = "liter", + Symbol = "l" + }, + new + { + Id = new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), + Name = "gramm", + Symbol = "g" + }, + new + { + Id = new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), + Name = "kilogramm", + Symbol = "kg" + }, + new + { + Id = new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), + Name = "stücke", + Symbol = "stk" + }, + new + { + Id = new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), + Name = "blatt", + Symbol = "blatt" + }, + new + { + Id = new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), + Name = "messerspitze", + Symbol = "msp" + }, + new + { + Id = new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), + Name = "stange", + Symbol = "stange" + }, + new + { + Id = new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"), + Name = "bund", + Symbol = "bund" + }, + new + { + Id = new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), + Name = "zehe", + Symbol = "zehe" + }, + new + { + Id = new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), + Name = "teelöffel", + Symbol = "TL" + }, + new + { + Id = new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), + Name = "esslöffel", + Symbol = "EL" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", null) + .WithMany("IngredientShoppingLists") + .HasForeignKey("IngredientId"); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", "RecipeIngredient") + .WithMany() + .HasForeignKey("RecipeIngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList") + .WithMany("SelectedIngredients") + .HasForeignKey("RecipeShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RecipeIngredient"); + + b.Navigation("RecipeShoppingList"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("Instructions") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", "Instruction") + .WithMany("MediaFiles") + .HasForeignKey("InstructionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("MediaFiles") + .HasForeignKey("RecipeId"); + + b.Navigation("Instruction"); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category") + .WithMany("Recipes") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit") + .WithMany("Recipe") + .HasForeignKey("FavoritId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Favorit"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient") + .WithMany("RecipeIngredients") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("RecipeIngredients") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit") + .WithMany("RecipeIngredient") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("Recipe"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany() + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") + .WithMany("RecipeIngredientShoppingLists") + .HasForeignKey("ShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + + b.Navigation("ShoppingList"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => + { + b.Navigation("Recipes"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => + { + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => + { + b.Navigation("IngredientShoppingLists"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.Navigation("MediaFiles"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.Navigation("Instructions"); + + b.Navigation("MediaFiles"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Navigation("SelectedIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => + { + b.Navigation("RecipeIngredientShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => + { + b.Navigation("RecipeIngredient"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs new file mode 100644 index 0000000..5caf69f --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs @@ -0,0 +1,162 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + /// + public partial class UpdateShoppingLIstLogic : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId", + table: "RecipeIngredientsShoppingLists"); + + migrationBuilder.DropForeignKey( + name: "FK_Recipes_Categories_CategoryId", + table: "Recipes"); + + migrationBuilder.RenameColumn( + name: "ShoppingListId", + table: "RecipeIngredientsShoppingLists", + newName: "RecipeShoppingListId"); + + migrationBuilder.RenameIndex( + name: "IX_RecipeIngredientsShoppingLists_ShoppingListId", + table: "RecipeIngredientsShoppingLists", + newName: "IX_RecipeIngredientsShoppingLists_RecipeShoppingListId"); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Recipes", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "IsChecked", + table: "RecipeIngredientsShoppingLists", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "RecipeShoppingLists", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ShoppingListId = table.Column(type: "uniqueidentifier", nullable: false), + RecipeId = table.Column(type: "uniqueidentifier", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_RecipeShoppingLists", x => x.Id); + table.ForeignKey( + name: "FK_RecipeShoppingLists_Recipes_RecipeId", + column: x => x.RecipeId, + principalTable: "Recipes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RecipeShoppingLists_ShoppingLists_ShoppingListId", + column: x => x.ShoppingListId, + principalTable: "ShoppingLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RecipeShoppingLists_RecipeId", + table: "RecipeShoppingLists", + column: "RecipeId"); + + migrationBuilder.CreateIndex( + name: "IX_RecipeShoppingLists_ShoppingListId", + table: "RecipeShoppingLists", + column: "ShoppingListId"); + + migrationBuilder.AddForeignKey( + name: "FK_RecipeIngredientsShoppingLists_RecipeShoppingLists_RecipeShoppingListId", + table: "RecipeIngredientsShoppingLists", + column: "RecipeShoppingListId", + principalTable: "RecipeShoppingLists", + principalColumn: "Id", + onDelete: ReferentialAction.NoAction); + + migrationBuilder.AddForeignKey( + name: "FK_Recipes_Categories_CategoryId", + table: "Recipes", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_RecipeIngredientsShoppingLists_RecipeShoppingLists_RecipeShoppingListId", + table: "RecipeIngredientsShoppingLists"); + + migrationBuilder.DropForeignKey( + name: "FK_Recipes_Categories_CategoryId", + table: "Recipes"); + + migrationBuilder.DropTable( + name: "RecipeShoppingLists"); + + migrationBuilder.DropColumn( + name: "IsChecked", + table: "RecipeIngredientsShoppingLists"); + + migrationBuilder.RenameColumn( + name: "RecipeShoppingListId", + table: "RecipeIngredientsShoppingLists", + newName: "ShoppingListId"); + + migrationBuilder.RenameIndex( + name: "IX_RecipeIngredientsShoppingLists_RecipeShoppingListId", + table: "RecipeIngredientsShoppingLists", + newName: "IX_RecipeIngredientsShoppingLists_ShoppingListId"); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Recipes", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + + migrationBuilder.AddForeignKey( + name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId", + table: "RecipeIngredientsShoppingLists", + column: "ShoppingListId", + principalTable: "ShoppingLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Recipes_Categories_CategoryId", + table: "Recipes", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id"); + } + } +} diff --git a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs index 6b432d8..c27540a 100644 --- a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs +++ b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs @@ -1,7 +1,10 @@ // +using System; using Francesco.Recipes.World.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable @@ -124,10 +127,13 @@ namespace Francesco.Recipes.World.Migrations b.Property("IngredientId") .HasColumnType("uniqueidentifier"); + b.Property("IsChecked") + .HasColumnType("bit"); + b.Property("RecipeIngredientId") .HasColumnType("uniqueidentifier"); - b.Property("ShoppingListId") + b.Property("RecipeShoppingListId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); @@ -136,7 +142,7 @@ namespace Francesco.Recipes.World.Migrations b.HasIndex("RecipeIngredientId"); - b.HasIndex("ShoppingListId"); + b.HasIndex("RecipeShoppingListId"); b.ToTable("RecipeIngredientsShoppingLists"); }); @@ -200,7 +206,7 @@ namespace Francesco.Recipes.World.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("CategoryId") + b.Property("CategoryId") .HasColumnType("uniqueidentifier"); b.Property("CookingTime") @@ -272,6 +278,27 @@ namespace Francesco.Recipes.World.Migrations b.ToTable("RecipeIngredients"); }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.HasIndex("ShoppingListId"); + + b.ToTable("RecipeShoppingLists"); + }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => { b.Property("Id") @@ -388,15 +415,15 @@ namespace Francesco.Recipes.World.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") - .WithMany("RecipeIngredientShoppingLists") - .HasForeignKey("ShoppingListId") + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList") + .WithMany("SelectedIngredients") + .HasForeignKey("RecipeShoppingListId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("RecipeIngredient"); - b.Navigation("ShoppingList"); + b.Navigation("RecipeShoppingList"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => @@ -429,9 +456,11 @@ namespace Francesco.Recipes.World.Migrations modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", null) + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category") .WithMany("Recipes") - .HasForeignKey("CategoryId"); + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit") .WithMany("Recipe") @@ -439,6 +468,8 @@ namespace Francesco.Recipes.World.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Category"); + b.Navigation("Favorit"); }); @@ -469,6 +500,25 @@ namespace Francesco.Recipes.World.Migrations b.Navigation("Unit"); }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany() + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") + .WithMany("RecipeIngredientShoppingLists") + .HasForeignKey("ShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + + b.Navigation("ShoppingList"); + }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => { b.Navigation("Recipes"); @@ -500,6 +550,11 @@ namespace Francesco.Recipes.World.Migrations b.Navigation("RecipeIngredients"); }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Navigation("SelectedIngredients"); + }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => { b.Navigation("RecipeIngredientShoppingLists"); diff --git a/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs index ee81fd2..5dc055b 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs @@ -1,15 +1,22 @@ -namespace Francesco.Recipes.World.Models.BackendModels.Recipe +using System.ComponentModel.DataAnnotations; + +namespace Francesco.Recipes.World.Models.BackendModels.Recipe { public enum Difficulty { + [Display(Name = "Sehr einfach")] VeryEasy = 0, + [Display(Name = "Einfach")] Easy = 1, + [Display(Name = "Mittel")] Medium = 2, + [Display(Name = "Schwer")] Hard = 3, + [Display(Name = "Experte")] Expert = 4, } } diff --git a/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs index 1878caf..fa691b8 100644 --- a/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs +++ b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs @@ -1,14 +1,16 @@ namespace Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList { using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; - using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; + using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; public class RecipeIngredientShoppingList { public Guid Id { get; set; } - public virtual ShoppingList ShoppingList { get; set; } = new (); + public virtual RecipeShoppingList RecipeShoppingList { get; set; } = new (); public virtual RecipeIngredient RecipeIngredient { get; set; } = new (); + + public bool IsChecked { get; set; } = false; } } diff --git a/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs new file mode 100644 index 0000000..1173425 --- /dev/null +++ b/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs @@ -0,0 +1,17 @@ +namespace Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList +{ + using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; + + public class RecipeShoppingList + { + public Guid Id { get; set; } + + public virtual ShoppingList ShoppingList { get; set; } = new ShoppingList(); + + public virtual Recipe Recipe { get; set; } = new (); + + public virtual ICollection SelectedIngredients { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs index c8fd12f..1a06a05 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs @@ -1,6 +1,6 @@ namespace Francesco.Recipes.World.Models.BackendModels.Shoppinglist { - using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; + using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; public class ShoppingList : ITimeStampedEntity { @@ -10,6 +10,6 @@ public DateTime? ModifiedAt { get; set; } - public virtual ICollection RecipeIngredientShoppingLists { get; set; } = new List(); + public virtual ICollection RecipeShoppingList { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index 719acda..6f98ff1 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -1,5 +1,6 @@ using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Repositories.Category; +using Francesco.Recipes.World.Repositories.Favorit; using Francesco.Recipes.World.Repositories.Ingredient; using Francesco.Recipes.World.Repositories.Instruction; using Francesco.Recipes.World.Repositories.MediaFile; @@ -7,8 +8,6 @@ using Francesco.Recipes.World.Repositories.Recipe; using Francesco.Recipes.World.Repositories.ShoppingList; using Francesco.Recipes.World.Repositories.Unit; -using Microsoft.AspNetCore.Identity; - using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -23,9 +22,6 @@ var connectionString = builder.Configuration.GetConnectionString("FrancescosReci services.AddDbContext(options => options.UseSqlServer(connectionString)); -services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) - .AddEntityFrameworkStores(); - // Add services to the container. builder.Services.AddControllersWithViews(); @@ -43,6 +39,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -62,10 +60,6 @@ app.UseStaticFiles(); app.UseRouting(); -app.UseAuthentication(); - -app.UseAuthorization(); - app.MapDefaultControllerRoute(); app.MapControllerRoute( diff --git a/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs b/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs new file mode 100644 index 0000000..1559dd8 --- /dev/null +++ b/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs @@ -0,0 +1,12 @@ +namespace Francesco.Recipes.World.Views.Category +{ + using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + + public class CategoryRecipesViewModel + { + public Category Category { get; set; } = new (); + + public IEnumerable Recipes { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml b/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml new file mode 100644 index 0000000..d6d5f79 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml @@ -0,0 +1,59 @@ +@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe + +@{ + ViewData["Title"] = "Add Instructions"; +} + +

Add Instructions to @Model.Name

+ +
+

Existing Instructions

+
    + @foreach (var instruction in Model.Instructions.OrderBy(i => i.Number)) + { +
  • @instruction.Number. @instruction.Description
  • + } +
+
+ +
+

Add New Instruction

+
+ +
+ + +
+
+ + +
+ +
+
+ +@section Scripts { + +} + diff --git a/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml index c72fdcf..1232469 100644 --- a/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml @@ -1,29 +1,27 @@ @{ - ViewData["Title"] = "Add or Create Ingredient to Recipe"; + ViewData["Title"] = "Add or Create Ingredient to Recipe"; }

@ViewData["Title"]

-
- - -
-
- - -
-
- - -
-
- - -
- + +
+ + +
+
+ + +
+
+ + +
+
@section Scripts { - + } + diff --git a/Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml b/Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml new file mode 100644 index 0000000..df06c6e --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml @@ -0,0 +1,98 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Category Recipes"; +} + +

Category Recipes

+ +@foreach (var categoryRecipes in Model) +{ +
+

@categoryRecipes.Category.Name

+ Rezept erstellen +
+ @foreach (var recipe in categoryRecipes.Recipes) + { +
+
+ @if (recipe.MediaFiles.Any() && recipe.MediaFiles.First().Data != null) + { + var mediaFile = recipe.MediaFiles.First(); + if (mediaFile.Data != null) + { + @recipe.Name + } + } +
+
+

@recipe.Name

+

@recipe.Description

+

Difficulty: @recipe.Difficulty

+

Servings: @recipe.Servings

+

Preparation Time: @recipe.PreparationTime

+

Cooking Time: @recipe.CookingTime

+
+ @if (recipe.IsFavorite) + { +
+ + +
+ } + else + { +
+ + +
+ } +
+
+
+ } + +
+
+} + + + diff --git a/Francesco.Recipes.World/Views/Recipe/Create.cshtml b/Francesco.Recipes.World/Views/Recipe/Create.cshtml index 8b1e201..e8c3769 100644 --- a/Francesco.Recipes.World/Views/Recipe/Create.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Create.cshtml @@ -1,45 +1,53 @@ @model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe @using Francesco.Recipes.World.Models.BackendModels.Recipe @{ - ViewData["Title"] = "Create Recipe"; + ViewData["Title"] = "Create Recipe"; }

Create Recipe

-
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- -
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
@section Scripts { diff --git a/Francesco.Recipes.World/Views/Recipe/Details.cshtml b/Francesco.Recipes.World/Views/Recipe/Details.cshtml new file mode 100644 index 0000000..4ea25c7 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/Details.cshtml @@ -0,0 +1,68 @@ +@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe + +@{ + ViewData["Title"] = "Recipe Details"; +} + +

@Model.Name

+ +
+
+ @if (Model.MediaFiles.Any() && Model.MediaFiles.First().Data != null) + { + var mediaFile = Model.MediaFiles.First(); + if (mediaFile.Data != null) + { + @Model.Name + } + } +
+
+

Description: @Model.Description

+

Difficulty: @Model.Difficulty

+

Servings: @Model.Servings

+

Preparation Time: @Model.PreparationTime

+

Cooking Time: @Model.CookingTime

+
+
+

Ingredients

+
+
    + @foreach (var ingredient in Model.RecipeIngredients) + { +
  • + + @ingredient.Ingredient.Name - @ingredient.Quantity @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty) +
  • + } +
+ +
+
+
+ +@section Scripts { + +} diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml index 01a77cb..b0e499e 100644 --- a/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml @@ -1,42 +1,72 @@ - @model Francesco.Recipes.World.Views.Recipe.FilterByDifficultyViewModel - @using Francesco.Recipes.World.Models.BackendModels.Recipe +@using Francesco.Recipes.World.Models.BackendModels.Recipe +@model Francesco.Recipes.World.Views.Recipe.FilterByDifficultyViewModel -@{ - ViewData["Title"] = "Filter Recipes by Difficulty"; -} +

Rezepte nach Schwierigkeitsgrad

-

Filter Recipes by Difficulty

+ -
-
- - - -
-
+
+
+
+
+ + +
+
-@if (Model.Recipes != null && Model.Recipes.Any()) -{ -

Filtered Recipes

-
    - @foreach (var recipe in Model.Recipes) - { -
  • @recipe.Name - @recipe.Difficulty.
  • - } -
-} -else -{ -

No recipes found for the selected difficulty.

-} +
+
+ +
+ @if (Model?.Recipes != null && Model.Recipes.Any()) + { +
+ + + + + + + + + + + + + @foreach (var recipe in Model.Recipes) + { + + + + + + + + + } + +
NameBeschreibungSchwierigkeitsgradPortionenZubereitungszeitAktionen
@recipe.Name@(recipe.Description?.Length > 100 ? recipe.Description.Substring(0, 100) + "..." : recipe.Description)@recipe.?Difficulty@recipe.Servings@($"{recipe.PreparationTime.TotalMinutes} Min.") + Details + Bearbeiten +
+
+ } + else + { +
+

Keine Rezepte gefunden.

+
+ } +
@section Scripts { - - @await Html.PartialAsync("_ValidationScriptsPartial") + } diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs b/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs index 94f71f6..b0543f3 100644 --- a/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs @@ -7,6 +7,6 @@ { public Difficulty? SelectedDifficulty { get; set; } - public List Recipes { get; set; } = new List(); + public IReadOnlyCollection Recipes { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml new file mode 100644 index 0000000..565c6f6 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml @@ -0,0 +1,43 @@ +@model IEnumerable + +
+

Zutaten

+
+
    + @foreach (var ingredient in Model) + { +
  • + + @ingredient.Ingredient.Name - @ingredient.Quantity @ingredient.Unit.Symbol +
  • + } +
+ +
+
+ +@section Scripts { + +} diff --git a/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml b/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml new file mode 100644 index 0000000..1efa149 --- /dev/null +++ b/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml @@ -0,0 +1,56 @@ +@model Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList +@using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList +@using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient +@using Francesco.Recipes.World.Models.BackendModels.Recipe + +@{ + ViewData["Title"] = "Einkaufsliste Details"; +} + +

Einkaufsliste Details

+ +@if (TempData["SuccessMessage"] != null) +{ +
+ @TempData["SuccessMessage"] +
+} + +
+

Einkaufsliste

+
+
+
+ ID +
+
+ @Model.Id +
+
+
+ +

Rezepte

+ + + + + + + + + @foreach (var recipeShoppingList in Model.RecipeShoppingList) + { + + + + + } + +
RezeptnameZutaten
@recipeShoppingList.Recipe.Name +
    + @foreach (var ingredient in recipeShoppingList.SelectedIngredients) + { +
  • @ingredient.RecipeIngredient.Ingredient.Name - @ingredient.RecipeIngredient.Quantity @ingredient.RecipeIngredient.Unit.Name
  • + } +
+
From 928743303c13a39ee1120cec3b27f7ad51055ce5 Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 8 Apr 2025 15:23:57 +0200 Subject: [PATCH 15/18] Revert "add CategoryController with endpoints" This reverts commit 36bca4d6584d644a81c2fd755d53ae6bea018419. --- .../Controller/Category/CategoryController.cs | 3 +- .../MediaFile/MediaFileController.cs | 44 +- .../Controller/Recipe/RecipeController.cs | 160 +---- .../ShoppingList/ShoppingListController.cs | 48 +- .../Data/FrancescosRecipesWorldDbContext.cs | 5 +- .../Francesco.Recipes.World.csproj | 10 +- ...120842_UpdateShoppingLIstLogic.Designer.cs | 572 ------------------ .../20250402120842_UpdateShoppingLIstLogic.cs | 162 ----- ...escosRecipesWorldDbContextModelSnapshot.cs | 73 +-- .../Models/BackendModels/Recipe/Difficulty.cs | 9 +- .../RecipeIngredientShoppingList.cs | 6 +- .../RecipeShoppingList/RecipeShoppingList.cs | 17 - .../Shoppinglist/ShoppingList.cs | 4 +- Francesco.Recipes.World/Program.cs | 12 +- .../Category/CategoryRecipesViewModel.cs | 12 - .../Views/Recipe/AddInstruction.cshtml | 59 -- .../Views/Recipe/AddOrCreateIngredient.cshtml | 36 +- .../Views/Recipe/CategoryRecipes.cshtml | 98 --- .../Views/Recipe/Create.cshtml | 78 ++- .../Views/Recipe/Details.cshtml | 68 --- .../Views/Recipe/FilterByDifficulty.cshtml | 100 ++- .../Recipe/FilterByDifficultyViewModel.cs | 2 +- ...AddIngredientsToShoppingListPartial.cshtml | 43 -- .../Views/ShoppingList/Details.cshtml | 56 -- 24 files changed, 142 insertions(+), 1535 deletions(-) delete mode 100644 Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs delete mode 100644 Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs delete mode 100644 Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs delete mode 100644 Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs delete mode 100644 Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml delete mode 100644 Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml delete mode 100644 Francesco.Recipes.World/Views/Recipe/Details.cshtml delete mode 100644 Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml delete mode 100644 Francesco.Recipes.World/Views/ShoppingList/Details.cshtml diff --git a/Francesco.Recipes.World/Controller/Category/CategoryController.cs b/Francesco.Recipes.World/Controller/Category/CategoryController.cs index 29b06b5..0257ab4 100644 --- a/Francesco.Recipes.World/Controller/Category/CategoryController.cs +++ b/Francesco.Recipes.World/Controller/Category/CategoryController.cs @@ -5,7 +5,6 @@ using Francesco.Recipes.World.Repositories.Category; using Microsoft.AspNetCore.Mvc; - [ValidateAntiForgeryToken] [Route("Category")] public class CategoryController : Controller @@ -38,7 +37,7 @@ public async Task>> GetRecipesByCategory(Guid id) { var recipes = await _categoryRepository.GetRecipesByCategoryAsync(id); - return View(recipes); + return Ok(recipes); } } } diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs index 14f59fe..6cec16d 100644 --- a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -1,48 +1,6 @@ namespace Francesco.Recipes.World.Controller.MediaFile { - using Francesco.Recipes.World.Data; - using Francesco.Recipes.World.Repositories.MediaFile; - using Microsoft.AspNetCore.Mvc; - - [ValidateAntiForgeryToken] - - [Route("categories/{categoryId}/Recipe")] - public class MediaFileController : Controller + public class MediaFileController { - private readonly IMediaFileRepository _mediaFileRepository; - private readonly FrancescosRecipesWorldDbContext _context; - - public MediaFileController(IMediaFileRepository mediaFileRepository, FrancescosRecipesWorldDbContext context) - { - _mediaFileRepository = mediaFileRepository; - _context = context; - } - - // POST: /UploadImage - [HttpPost("UploadImage")] - public async Task UploadImage(Guid recipeId, IFormFile? mediaFile) - { - if (mediaFile is null) - { - return BadRequest("Photo is required."); - } - - try - { - await _mediaFileRepository.UploadRecipeMediaAsync(recipeId, mediaFile); - return Ok("Image uploaded successfully."); - } - catch (Exception ex) - { - return StatusCode(500, $"Internal server error: {ex.Message}"); - } - } - - // GET: /UploadImage - [HttpGet("UploadImage")] - public IActionResult UploadImageView() - { - return View(); - } } } diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 05d99aa..9b4cd9e 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -1,94 +1,43 @@ namespace Francesco.Recipes.World.Controller.Recipe { - using System.ComponentModel.DataAnnotations; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Repositories.Category; - using Francesco.Recipes.World.Repositories.Favorit; using Francesco.Recipes.World.Repositories.Ingredient; - using Francesco.Recipes.World.Repositories.Instruction; - using Francesco.Recipes.World.Repositories.MediaFile; using Francesco.Recipes.World.Repositories.Recipe; using Francesco.Recipes.World.Repositories.Unit; - using Francesco.Recipes.World.Views.Category; - using Francesco.Recipes.World.Views.Recipe; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; - [ValidateAntiForgeryToken] - [Route("Recipe")] + [Route("categories/{categoryId}/Recipe")] public class RecipeController : Controller { private readonly IRecipeRepository _recipeRepository; private readonly IUnitRepository _unitRepository; private readonly ICategoryRepository _categoryRepository; private readonly IIngredientRepository _ingredientRepository; - private readonly IMediaFileRepository _mediaFileRepository; - private readonly IInstructionRepository _instructionRepository; - private readonly IFavoritRepository _favoritRepository; - [Display(Name = "Schwierigkeitsgrad")] - [BindProperty(SupportsGet = true)] - public Difficulty? SelectedDifficulty { get; set; } - - public RecipeController(IRecipeRepository recipeRepository, IUnitRepository unitRepository, ICategoryRepository categoryRepository, IIngredientRepository ingredientRepository, IMediaFileRepository mediaFileRepository, IInstructionRepository instructionRepository, IFavoritRepository favoritRepository) + public RecipeController(IRecipeRepository recipeRepository, IUnitRepository unitRepository, ICategoryRepository categoryRepository, IIngredientRepository ingredientRepository) { _recipeRepository = recipeRepository; _unitRepository = unitRepository; _categoryRepository = categoryRepository; _ingredientRepository = ingredientRepository; - Recipes = new List(); - _mediaFileRepository = mediaFileRepository; - _instructionRepository = instructionRepository; - _favoritRepository = favoritRepository; } public IReadOnlyCollection Recipes { get; set; } - // GET: /Recipe/{recipeId}/AddOrCreateIngredient + // GET: /Recipe/AddOrCreateIngredient [HttpGet("{recipeId}/AddOrCreateIngredient")] - public async Task AddOrCreateIngredient(Guid recipeId) + public async Task AddOrCreateIngredient() { var units = await _unitRepository.GetAllUnitsAsync(); ViewBag.Units = new SelectList(units, "Id", "Name"); - ViewBag.RecipeId = recipeId; return View(); } - // GET: /Recipe/Details/{recipeId} - [HttpGet("Details/{recipeId}")] - public async Task Details(Guid recipeId) - { - var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); - if (recipe == null) - { - return NotFound("Recipe not found."); - } - - return View(recipe); - } - - // GET: /Recipe/CategoryRecipes - [HttpGet("CategoryRecipes")] - public async Task CategoryRecipes() - { - var categories = await _categoryRepository.GetAllCategoriesAsync(); - var viewModel = new List(); - - foreach (var category in categories) - { - var recipes = await _categoryRepository.GetRecipesByCategoryAsync(category.Id); - viewModel.Add(new CategoryRecipesViewModel - { - Category = category, - Recipes = recipes, - }); - } - - return View(viewModel); - } - - // POST: /Recipe/{recipeId}/AddOrCreateIngredient + // POST: /Recipe/AddOrCreateIngredient [HttpPost("{recipeId}/AddOrCreateIngredient")] + [ValidateAntiForgeryToken] public async Task AddOrCreateIngredient(Guid recipeId, string ingredientName, int quantity, Guid unitId) { if (quantity <= 0) @@ -107,8 +56,8 @@ return RedirectToAction("Details", new { id = recipeId }); } - // GET: /Recipe/Create/{categoryId} - [HttpGet("Create/{categoryId}")] + // GET: /categories/{categoryId}/Recipe/Create + [HttpGet("Create")] public async Task Create(Guid categoryId) { var category = await _categoryRepository.GetCategoryByIdAsync(categoryId); @@ -121,9 +70,10 @@ return View(); } - // POST: /Recipe/Create/{categoryId} - [HttpPost("Create/{categoryId}")] - public async Task Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime, IFormFile? photo) + // POST: /categories/{categoryId}/Recipe/Create + [HttpPost("Create")] + [ValidateAntiForgeryToken] + public async Task Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime) { if (string.IsNullOrWhiteSpace(name)) { @@ -153,18 +103,13 @@ return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } - var recipe = await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); - if (photo != null) - { - await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, photo); - } - + await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); return RedirectToAction("Details", "Category", new { id = categoryId }); } - // GET: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} + // GET: /categories/{categoryId}/Recipe/{recipeId}/RemoveIngredient/{ingredientId} [HttpGet("{recipeId}/RemoveIngredient/{ingredientId}")] - public async Task RemoveIngredient(Guid recipeId, Guid ingredientId) + public async Task RemoveIngredient(Guid categoryId, Guid recipeId, Guid ingredientId) { var recipe = await _recipeRepository.GetRecipeAsync(recipeId); var ingredient = await _ingredientRepository.GetIngredientByIdAsync(ingredientId); @@ -176,87 +121,28 @@ ViewBag.RecipeId = recipeId; ViewBag.IngredientId = ingredientId; + ViewBag.CategoryId = categoryId; ViewBag.IngredientName = ingredient.Name; return View(); } - // POST: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} + // POST: /categories/{categoryId}/Recipe/{recipeId}/RemoveIngredient/{ingredientId} [HttpPost("{recipeId}/RemoveIngredient/{ingredientId}")] - public async Task RemoveIngredientConfirmed(Guid recipeId, Guid ingredientId) + [ValidateAntiForgeryToken] + public async Task RemoveIngredientConfirmed(Guid categoryId, Guid recipeId, Guid ingredientId) { await _recipeRepository.RemoveIngredientFromRecipeAsync(recipeId, ingredientId); TempData["SuccessMessage"] = "Ingredient removed successfully."; return RedirectToAction("Details", new { id = recipeId }); } - // GET: /Recipe/FilterByDifficulty + // GET: /categories/{categoryId}/Recipe/FilterByDifficulty [HttpGet("FilterByDifficulty")] - public async Task FilterByDifficulty(Difficulty? selectedDifficulty) + public async Task FilterByDifficulty(Difficulty? difficulty) { - var recipes = await _recipeRepository.GetRecipesByDifficultyAsync(selectedDifficulty); - var viewModel = new FilterByDifficultyViewModel - { - SelectedDifficulty = selectedDifficulty, - Recipes = recipes, - }; - return View(viewModel); - } - - // GET: /Recipe/{recipeId}/AddInstruction - [HttpGet("{recipeId}/AddInstruction")] - public async Task AddInstruction(Guid recipeId) - { - var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); - if (recipe == null) - { - return NotFound("Recipe not found."); - } - - ViewBag.RecipeId = recipeId; - return View(recipe); - } - - // POST: /Recipe/{recipeId}/AddInstruction - [HttpPost("{recipeId}/AddInstruction")] - public async Task AddInstruction(Guid recipeId, string description, int number) - { - try - { - await _instructionRepository.CreateInstructionToRecipeAsync(recipeId, description, number); - return RedirectToAction("AddInstruction", new { recipeId }); - } - catch (Exception ex) - { - ModelState.AddModelError(string.Empty, ex.Message); - var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); - ViewBag.RecipeId = recipeId; - return View(recipe); - } - } - - // GET: /Recipe/Favorites - [HttpGet("Favorites")] - public async Task Favorites() - { - var favoriteRecipes = await _favoritRepository.GetFavoriteRecipesAsync(); - return View(favoriteRecipes); - } - - // POST: /Recipe/AddFavorite - [HttpPost("AddFavorite")] - public async Task AddFavorite(Guid recipeId) - { - await _favoritRepository.AddFavoriteAsync(recipeId); - return RedirectToAction("Details", new { recipeId }); - } - - // POST: /Recipe/RemoveFavorite - [HttpPost("RemoveFavorite")] - public async Task RemoveFavorite(Guid recipeId) - { - await _favoritRepository.RemoveFavoriteAsync(recipeId); - return RedirectToAction("Details", new { recipeId }); + Recipes = await _recipeRepository.GetRecipesByDifficultyAsync(difficulty); + return View(Recipes); } } } diff --git a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs index 7f6a434..38fdf4b 100644 --- a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs +++ b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs @@ -1,50 +1,6 @@ -namespace Francesco.Recipes.World.Controllers +namespace Francesco.Recipes.World.Controller.ShoppingList { - using Francesco.Recipes.World.Data; - using Francesco.Recipes.World.Repositories.ShoppingList; - using Microsoft.AspNetCore.Mvc; - using Microsoft.EntityFrameworkCore; - - [Route("ShoppingList")] - public class ShoppingListController : Controller + public class ShoppingListController { - private readonly IShoppingListRepository _shoppingListRepository; - private readonly FrancescosRecipesWorldDbContext _context; - - public ShoppingListController(IShoppingListRepository shoppingListRepository, FrancescosRecipesWorldDbContext context) - { - _shoppingListRepository = shoppingListRepository; - _context = context; - } - - [HttpPost("CreateOrAddIngredients")] - public async Task CreateOrAddIngredients([FromBody] CreateOrAddIngredientsRequest request) - { - if (request == null || request.IngredientIds == null || !request.IngredientIds.Any()) - { - return BadRequest("No ingredients provided."); - } - - await _shoppingListRepository.AddIngredientsToShoppingListAsync(request.RecipeId, request.IngredientIds); - - var shoppingList = await _context.ShoppingLists - .Include(sl => sl.RecipeShoppingList) - .ThenInclude(rsl => rsl.Recipe) - .FirstOrDefaultAsync(sl => sl.RecipeShoppingList.Any(rsl => rsl.Recipe.Id == request.RecipeId)); - - if (shoppingList == null) - { - return BadRequest("Error creating shopping list."); - } - - return Json(new { shoppingListId = shoppingList.Id }); - } - - public class CreateOrAddIngredientsRequest - { - public Guid RecipeId { get; set; } - - public List IngredientIds { get; set; } = new (); - } } } diff --git a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs index 7a8a5e7..4fd6bd8 100644 --- a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs +++ b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs @@ -9,8 +9,7 @@ using Francesco.Recipes.World.Models.BackendModels.MediaFile; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; -using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; -using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; + using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; using Francesco.Recipes.World.Models.BackendModels.Unit; using Microsoft.EntityFrameworkCore; @@ -37,8 +36,6 @@ using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; public DbSet RecipeIngredientsShoppingLists => Set(); - public DbSet RecipeShoppingLists => Set(); - public DbSet ShoppingLists => Set(); public DbSet MediaFiles => Set(); diff --git a/Francesco.Recipes.World/Francesco.Recipes.World.csproj b/Francesco.Recipes.World/Francesco.Recipes.World.csproj index 7d430a2..549a91d 100644 --- a/Francesco.Recipes.World/Francesco.Recipes.World.csproj +++ b/Francesco.Recipes.World/Francesco.Recipes.World.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -45,13 +45,5 @@ - - - - - - - - diff --git a/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs deleted file mode 100644 index d99ae26..0000000 --- a/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs +++ /dev/null @@ -1,572 +0,0 @@ -// - -using Francesco.Recipes.World.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - - -#nullable disable - -namespace Francesco.Recipes.World.Migrations -{ - [DbContext(typeof(FrancescosRecipesWorldDbContext))] - [Migration("20250402120842_UpdateShoppingLIstLogic")] - partial class UpdateShoppingLIstLogic - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Categories"); - - b.HasData( - new - { - Id = new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"), - Name = "Vorspeisen & Snacks" - }, - new - { - Id = new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), - Name = "Erste Gänge" - }, - new - { - Id = new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), - Name = "Hauptgerichte" - }, - new - { - Id = new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"), - Name = "Desserts & Süßspeisen" - }, - new - { - Id = new Guid("28e39168-701a-4084-81da-d96c987c462f"), - Name = "Beilagen & Salate" - }, - new - { - Id = new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"), - Name = "Kuchen" - }, - new - { - Id = new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), - Name = "Hefegebäck & Brot" - }, - new - { - Id = new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"), - Name = "Soßen & Saucen" - }, - new - { - Id = new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), - Name = "Marmeladen & Eingemachtes" - }, - new - { - Id = new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), - Name = "Getränke" - }); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("CreatedAt") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.ToTable("Favorits"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Ingredients"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("IngredientId") - .HasColumnType("uniqueidentifier"); - - b.Property("IsChecked") - .HasColumnType("bit"); - - b.Property("RecipeIngredientId") - .HasColumnType("uniqueidentifier"); - - b.Property("RecipeShoppingListId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("Id"); - - b.HasIndex("IngredientId"); - - b.HasIndex("RecipeIngredientId"); - - b.HasIndex("RecipeShoppingListId"); - - b.ToTable("RecipeIngredientsShoppingLists"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Description") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Number") - .HasColumnType("int"); - - b.Property("RecipeId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("Id"); - - b.HasIndex("RecipeId"); - - b.ToTable("Instructions"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Data") - .HasColumnType("varbinary(max)"); - - b.Property("FileName") - .HasColumnType("nvarchar(max)"); - - b.Property("InstructionId") - .HasColumnType("uniqueidentifier"); - - b.Property("MimeType") - .HasColumnType("nvarchar(max)"); - - b.Property("RecipeId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("Id"); - - b.HasIndex("InstructionId"); - - b.HasIndex("RecipeId"); - - b.ToTable("MediaFiles"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("CategoryId") - .HasColumnType("uniqueidentifier"); - - b.Property("CookingTime") - .HasColumnType("time"); - - b.Property("CreatedAt") - .HasColumnType("datetime2"); - - b.Property("Description") - .HasColumnType("nvarchar(max)"); - - b.Property("Difficulty") - .HasColumnType("int"); - - b.Property("FavoritId") - .HasColumnType("uniqueidentifier"); - - b.Property("IsFavorite") - .HasColumnType("bit"); - - b.Property("ModifiedAt") - .HasColumnType("datetime2"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("PreparationTime") - .HasColumnType("time"); - - b.Property("Servings") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("CategoryId"); - - b.HasIndex("FavoritId"); - - b.ToTable("Recipes"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("IngredientId") - .HasColumnType("uniqueidentifier"); - - b.Property("Quantity") - .HasColumnType("int"); - - b.Property("RecipeId") - .HasColumnType("uniqueidentifier"); - - b.Property("UnitId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("Id"); - - b.HasIndex("IngredientId"); - - b.HasIndex("RecipeId"); - - b.HasIndex("UnitId"); - - b.ToTable("RecipeIngredients"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("RecipeId") - .HasColumnType("uniqueidentifier"); - - b.Property("ShoppingListId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("Id"); - - b.HasIndex("RecipeId"); - - b.HasIndex("ShoppingListId"); - - b.ToTable("RecipeShoppingLists"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("CreatedAt") - .HasColumnType("datetime2"); - - b.Property("ModifiedAt") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.ToTable("ShoppingLists"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Symbol") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Units"); - - b.HasData( - new - { - Id = new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), - Name = "liter", - Symbol = "l" - }, - new - { - Id = new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), - Name = "gramm", - Symbol = "g" - }, - new - { - Id = new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), - Name = "kilogramm", - Symbol = "kg" - }, - new - { - Id = new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), - Name = "stücke", - Symbol = "stk" - }, - new - { - Id = new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), - Name = "blatt", - Symbol = "blatt" - }, - new - { - Id = new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), - Name = "messerspitze", - Symbol = "msp" - }, - new - { - Id = new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), - Name = "stange", - Symbol = "stange" - }, - new - { - Id = new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"), - Name = "bund", - Symbol = "bund" - }, - new - { - Id = new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), - Name = "zehe", - Symbol = "zehe" - }, - new - { - Id = new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), - Name = "teelöffel", - Symbol = "TL" - }, - new - { - Id = new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), - Name = "esslöffel", - Symbol = "EL" - }); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", null) - .WithMany("IngredientShoppingLists") - .HasForeignKey("IngredientId"); - - b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", "RecipeIngredient") - .WithMany() - .HasForeignKey("RecipeIngredientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList") - .WithMany("SelectedIngredients") - .HasForeignKey("RecipeShoppingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("RecipeIngredient"); - - b.Navigation("RecipeShoppingList"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") - .WithMany("Instructions") - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", "Instruction") - .WithMany("MediaFiles") - .HasForeignKey("InstructionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") - .WithMany("MediaFiles") - .HasForeignKey("RecipeId"); - - b.Navigation("Instruction"); - - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category") - .WithMany("Recipes") - .HasForeignKey("CategoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit") - .WithMany("Recipe") - .HasForeignKey("FavoritId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Category"); - - b.Navigation("Favorit"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient") - .WithMany("RecipeIngredients") - .HasForeignKey("IngredientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") - .WithMany("RecipeIngredients") - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit") - .WithMany("RecipeIngredient") - .HasForeignKey("UnitId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Ingredient"); - - b.Navigation("Recipe"); - - b.Navigation("Unit"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") - .WithMany() - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") - .WithMany("RecipeIngredientShoppingLists") - .HasForeignKey("ShoppingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Recipe"); - - b.Navigation("ShoppingList"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => - { - b.Navigation("Recipes"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => - { - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => - { - b.Navigation("IngredientShoppingLists"); - - b.Navigation("RecipeIngredients"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => - { - b.Navigation("MediaFiles"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => - { - b.Navigation("Instructions"); - - b.Navigation("MediaFiles"); - - b.Navigation("RecipeIngredients"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => - { - b.Navigation("SelectedIngredients"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => - { - b.Navigation("RecipeIngredientShoppingLists"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => - { - b.Navigation("RecipeIngredient"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs deleted file mode 100644 index 5caf69f..0000000 --- a/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs +++ /dev/null @@ -1,162 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Francesco.Recipes.World.Migrations -{ - /// - public partial class UpdateShoppingLIstLogic : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - if (migrationBuilder is null) - { - throw new ArgumentNullException(nameof(migrationBuilder)); - } - - migrationBuilder.DropForeignKey( - name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId", - table: "RecipeIngredientsShoppingLists"); - - migrationBuilder.DropForeignKey( - name: "FK_Recipes_Categories_CategoryId", - table: "Recipes"); - - migrationBuilder.RenameColumn( - name: "ShoppingListId", - table: "RecipeIngredientsShoppingLists", - newName: "RecipeShoppingListId"); - - migrationBuilder.RenameIndex( - name: "IX_RecipeIngredientsShoppingLists_ShoppingListId", - table: "RecipeIngredientsShoppingLists", - newName: "IX_RecipeIngredientsShoppingLists_RecipeShoppingListId"); - - migrationBuilder.AlterColumn( - name: "CategoryId", - table: "Recipes", - type: "uniqueidentifier", - nullable: false, - defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), - oldClrType: typeof(Guid), - oldType: "uniqueidentifier", - oldNullable: true); - - migrationBuilder.AddColumn( - name: "IsChecked", - table: "RecipeIngredientsShoppingLists", - type: "bit", - nullable: false, - defaultValue: false); - - migrationBuilder.CreateTable( - name: "RecipeShoppingLists", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - ShoppingListId = table.Column(type: "uniqueidentifier", nullable: false), - RecipeId = table.Column(type: "uniqueidentifier", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("PK_RecipeShoppingLists", x => x.Id); - table.ForeignKey( - name: "FK_RecipeShoppingLists_Recipes_RecipeId", - column: x => x.RecipeId, - principalTable: "Recipes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_RecipeShoppingLists_ShoppingLists_ShoppingListId", - column: x => x.ShoppingListId, - principalTable: "ShoppingLists", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_RecipeShoppingLists_RecipeId", - table: "RecipeShoppingLists", - column: "RecipeId"); - - migrationBuilder.CreateIndex( - name: "IX_RecipeShoppingLists_ShoppingListId", - table: "RecipeShoppingLists", - column: "ShoppingListId"); - - migrationBuilder.AddForeignKey( - name: "FK_RecipeIngredientsShoppingLists_RecipeShoppingLists_RecipeShoppingListId", - table: "RecipeIngredientsShoppingLists", - column: "RecipeShoppingListId", - principalTable: "RecipeShoppingLists", - principalColumn: "Id", - onDelete: ReferentialAction.NoAction); - - migrationBuilder.AddForeignKey( - name: "FK_Recipes_Categories_CategoryId", - table: "Recipes", - column: "CategoryId", - principalTable: "Categories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - if (migrationBuilder is null) - { - throw new ArgumentNullException(nameof(migrationBuilder)); - } - - migrationBuilder.DropForeignKey( - name: "FK_RecipeIngredientsShoppingLists_RecipeShoppingLists_RecipeShoppingListId", - table: "RecipeIngredientsShoppingLists"); - - migrationBuilder.DropForeignKey( - name: "FK_Recipes_Categories_CategoryId", - table: "Recipes"); - - migrationBuilder.DropTable( - name: "RecipeShoppingLists"); - - migrationBuilder.DropColumn( - name: "IsChecked", - table: "RecipeIngredientsShoppingLists"); - - migrationBuilder.RenameColumn( - name: "RecipeShoppingListId", - table: "RecipeIngredientsShoppingLists", - newName: "ShoppingListId"); - - migrationBuilder.RenameIndex( - name: "IX_RecipeIngredientsShoppingLists_RecipeShoppingListId", - table: "RecipeIngredientsShoppingLists", - newName: "IX_RecipeIngredientsShoppingLists_ShoppingListId"); - - migrationBuilder.AlterColumn( - name: "CategoryId", - table: "Recipes", - type: "uniqueidentifier", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uniqueidentifier"); - - migrationBuilder.AddForeignKey( - name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId", - table: "RecipeIngredientsShoppingLists", - column: "ShoppingListId", - principalTable: "ShoppingLists", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_Recipes_Categories_CategoryId", - table: "Recipes", - column: "CategoryId", - principalTable: "Categories", - principalColumn: "Id"); - } - } -} diff --git a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs index c27540a..6b432d8 100644 --- a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs +++ b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs @@ -1,10 +1,7 @@ // -using System; using Francesco.Recipes.World.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable @@ -127,13 +124,10 @@ namespace Francesco.Recipes.World.Migrations b.Property("IngredientId") .HasColumnType("uniqueidentifier"); - b.Property("IsChecked") - .HasColumnType("bit"); - b.Property("RecipeIngredientId") .HasColumnType("uniqueidentifier"); - b.Property("RecipeShoppingListId") + b.Property("ShoppingListId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); @@ -142,7 +136,7 @@ namespace Francesco.Recipes.World.Migrations b.HasIndex("RecipeIngredientId"); - b.HasIndex("RecipeShoppingListId"); + b.HasIndex("ShoppingListId"); b.ToTable("RecipeIngredientsShoppingLists"); }); @@ -206,7 +200,7 @@ namespace Francesco.Recipes.World.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("CategoryId") + b.Property("CategoryId") .HasColumnType("uniqueidentifier"); b.Property("CookingTime") @@ -278,27 +272,6 @@ namespace Francesco.Recipes.World.Migrations b.ToTable("RecipeIngredients"); }); - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("RecipeId") - .HasColumnType("uniqueidentifier"); - - b.Property("ShoppingListId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("Id"); - - b.HasIndex("RecipeId"); - - b.HasIndex("ShoppingListId"); - - b.ToTable("RecipeShoppingLists"); - }); - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => { b.Property("Id") @@ -415,15 +388,15 @@ namespace Francesco.Recipes.World.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList") - .WithMany("SelectedIngredients") - .HasForeignKey("RecipeShoppingListId") + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") + .WithMany("RecipeIngredientShoppingLists") + .HasForeignKey("ShoppingListId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("RecipeIngredient"); - b.Navigation("RecipeShoppingList"); + b.Navigation("ShoppingList"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => @@ -456,11 +429,9 @@ namespace Francesco.Recipes.World.Migrations modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category") + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", null) .WithMany("Recipes") - .HasForeignKey("CategoryId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("CategoryId"); b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit") .WithMany("Recipe") @@ -468,8 +439,6 @@ namespace Francesco.Recipes.World.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Category"); - b.Navigation("Favorit"); }); @@ -500,25 +469,6 @@ namespace Francesco.Recipes.World.Migrations b.Navigation("Unit"); }); - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") - .WithMany() - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") - .WithMany("RecipeIngredientShoppingLists") - .HasForeignKey("ShoppingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Recipe"); - - b.Navigation("ShoppingList"); - }); - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => { b.Navigation("Recipes"); @@ -550,11 +500,6 @@ namespace Francesco.Recipes.World.Migrations b.Navigation("RecipeIngredients"); }); - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => - { - b.Navigation("SelectedIngredients"); - }); - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => { b.Navigation("RecipeIngredientShoppingLists"); diff --git a/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs index 5dc055b..ee81fd2 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs @@ -1,22 +1,15 @@ -using System.ComponentModel.DataAnnotations; - -namespace Francesco.Recipes.World.Models.BackendModels.Recipe +namespace Francesco.Recipes.World.Models.BackendModels.Recipe { public enum Difficulty { - [Display(Name = "Sehr einfach")] VeryEasy = 0, - [Display(Name = "Einfach")] Easy = 1, - [Display(Name = "Mittel")] Medium = 2, - [Display(Name = "Schwer")] Hard = 3, - [Display(Name = "Experte")] Expert = 4, } } diff --git a/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs index fa691b8..1878caf 100644 --- a/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs +++ b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs @@ -1,16 +1,14 @@ namespace Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList { using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; - using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; + using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; public class RecipeIngredientShoppingList { public Guid Id { get; set; } - public virtual RecipeShoppingList RecipeShoppingList { get; set; } = new (); + public virtual ShoppingList ShoppingList { get; set; } = new (); public virtual RecipeIngredient RecipeIngredient { get; set; } = new (); - - public bool IsChecked { get; set; } = false; } } diff --git a/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs deleted file mode 100644 index 1173425..0000000 --- a/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList -{ - using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; - using Francesco.Recipes.World.Models.BackendModels.Recipe; - using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; - - public class RecipeShoppingList - { - public Guid Id { get; set; } - - public virtual ShoppingList ShoppingList { get; set; } = new ShoppingList(); - - public virtual Recipe Recipe { get; set; } = new (); - - public virtual ICollection SelectedIngredients { get; set; } = new List(); - } -} diff --git a/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs index 1a06a05..c8fd12f 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs @@ -1,6 +1,6 @@ namespace Francesco.Recipes.World.Models.BackendModels.Shoppinglist { - using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; + using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; public class ShoppingList : ITimeStampedEntity { @@ -10,6 +10,6 @@ public DateTime? ModifiedAt { get; set; } - public virtual ICollection RecipeShoppingList { get; set; } = new List(); + public virtual ICollection RecipeIngredientShoppingLists { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index 6f98ff1..719acda 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -1,6 +1,5 @@ using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Repositories.Category; -using Francesco.Recipes.World.Repositories.Favorit; using Francesco.Recipes.World.Repositories.Ingredient; using Francesco.Recipes.World.Repositories.Instruction; using Francesco.Recipes.World.Repositories.MediaFile; @@ -8,6 +7,8 @@ using Francesco.Recipes.World.Repositories.Recipe; using Francesco.Recipes.World.Repositories.ShoppingList; using Francesco.Recipes.World.Repositories.Unit; +using Microsoft.AspNetCore.Identity; + using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -22,6 +23,9 @@ var connectionString = builder.Configuration.GetConnectionString("FrancescosReci services.AddDbContext(options => options.UseSqlServer(connectionString)); +services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores(); + // Add services to the container. builder.Services.AddControllersWithViews(); @@ -39,8 +43,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); - var app = builder.Build(); // Configure the HTTP request pipeline. @@ -60,6 +62,10 @@ app.UseStaticFiles(); app.UseRouting(); +app.UseAuthentication(); + +app.UseAuthorization(); + app.MapDefaultControllerRoute(); app.MapControllerRoute( diff --git a/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs b/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs deleted file mode 100644 index 1559dd8..0000000 --- a/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Francesco.Recipes.World.Views.Category -{ - using Francesco.Recipes.World.Models.BackendModels.Category; - using Francesco.Recipes.World.Models.BackendModels.Recipe; - - public class CategoryRecipesViewModel - { - public Category Category { get; set; } = new (); - - public IEnumerable Recipes { get; set; } = new List(); - } -} diff --git a/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml b/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml deleted file mode 100644 index d6d5f79..0000000 --- a/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml +++ /dev/null @@ -1,59 +0,0 @@ -@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe - -@{ - ViewData["Title"] = "Add Instructions"; -} - -

Add Instructions to @Model.Name

- -
-

Existing Instructions

-
    - @foreach (var instruction in Model.Instructions.OrderBy(i => i.Number)) - { -
  • @instruction.Number. @instruction.Description
  • - } -
-
- -
-

Add New Instruction

-
- -
- - -
-
- - -
- -
-
- -@section Scripts { - -} - diff --git a/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml index 1232469..c72fdcf 100644 --- a/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml @@ -1,27 +1,29 @@ @{ - ViewData["Title"] = "Add or Create Ingredient to Recipe"; + ViewData["Title"] = "Add or Create Ingredient to Recipe"; }

@ViewData["Title"]

- -
- - -
-
- - -
-
- - -
- +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@section Scripts { - + } - diff --git a/Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml b/Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml deleted file mode 100644 index df06c6e..0000000 --- a/Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml +++ /dev/null @@ -1,98 +0,0 @@ -@model IEnumerable - -@{ - ViewData["Title"] = "Category Recipes"; -} - -

Category Recipes

- -@foreach (var categoryRecipes in Model) -{ -
-

@categoryRecipes.Category.Name

- Rezept erstellen -
- @foreach (var recipe in categoryRecipes.Recipes) - { -
-
- @if (recipe.MediaFiles.Any() && recipe.MediaFiles.First().Data != null) - { - var mediaFile = recipe.MediaFiles.First(); - if (mediaFile.Data != null) - { - @recipe.Name - } - } -
-
-

@recipe.Name

-

@recipe.Description

-

Difficulty: @recipe.Difficulty

-

Servings: @recipe.Servings

-

Preparation Time: @recipe.PreparationTime

-

Cooking Time: @recipe.CookingTime

-
- @if (recipe.IsFavorite) - { -
- - -
- } - else - { -
- - -
- } -
-
-
- } - -
-
-} - - - diff --git a/Francesco.Recipes.World/Views/Recipe/Create.cshtml b/Francesco.Recipes.World/Views/Recipe/Create.cshtml index e8c3769..8b1e201 100644 --- a/Francesco.Recipes.World/Views/Recipe/Create.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Create.cshtml @@ -1,53 +1,45 @@ @model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe @using Francesco.Recipes.World.Models.BackendModels.Recipe @{ - ViewData["Title"] = "Create Recipe"; + ViewData["Title"] = "Create Recipe"; }

Create Recipe

-
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - -
- + +
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
@section Scripts { diff --git a/Francesco.Recipes.World/Views/Recipe/Details.cshtml b/Francesco.Recipes.World/Views/Recipe/Details.cshtml deleted file mode 100644 index 4ea25c7..0000000 --- a/Francesco.Recipes.World/Views/Recipe/Details.cshtml +++ /dev/null @@ -1,68 +0,0 @@ -@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe - -@{ - ViewData["Title"] = "Recipe Details"; -} - -

@Model.Name

- -
-
- @if (Model.MediaFiles.Any() && Model.MediaFiles.First().Data != null) - { - var mediaFile = Model.MediaFiles.First(); - if (mediaFile.Data != null) - { - @Model.Name - } - } -
-
-

Description: @Model.Description

-

Difficulty: @Model.Difficulty

-

Servings: @Model.Servings

-

Preparation Time: @Model.PreparationTime

-

Cooking Time: @Model.CookingTime

-
-
-

Ingredients

-
-
    - @foreach (var ingredient in Model.RecipeIngredients) - { -
  • - - @ingredient.Ingredient.Name - @ingredient.Quantity @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty) -
  • - } -
- -
-
-
- -@section Scripts { - -} diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml index b0e499e..01a77cb 100644 --- a/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml @@ -1,72 +1,42 @@ -@using Francesco.Recipes.World.Models.BackendModels.Recipe -@model Francesco.Recipes.World.Views.Recipe.FilterByDifficultyViewModel + @model Francesco.Recipes.World.Views.Recipe.FilterByDifficultyViewModel + @using Francesco.Recipes.World.Models.BackendModels.Recipe -

Rezepte nach Schwierigkeitsgrad

+@{ + ViewData["Title"] = "Filter Recipes by Difficulty"; +} - +

Filter Recipes by Difficulty

-
-
-
-
- - -
-
+
+
+ + + +
+
-
-
- -
- @if (Model?.Recipes != null && Model.Recipes.Any()) - { -
- - - - - - - - - - - - - @foreach (var recipe in Model.Recipes) - { - - - - - - - - - } - -
NameBeschreibungSchwierigkeitsgradPortionenZubereitungszeitAktionen
@recipe.Name@(recipe.Description?.Length > 100 ? recipe.Description.Substring(0, 100) + "..." : recipe.Description)@recipe.?Difficulty@recipe.Servings@($"{recipe.PreparationTime.TotalMinutes} Min.") - Details - Bearbeiten -
-
- } - else - { -
-

Keine Rezepte gefunden.

-
- } -
+@if (Model.Recipes != null && Model.Recipes.Any()) +{ +

Filtered Recipes

+
    + @foreach (var recipe in Model.Recipes) + { +
  • @recipe.Name - @recipe.Difficulty.
  • + } +
+} +else +{ +

No recipes found for the selected difficulty.

+} @section Scripts { - + + @await Html.PartialAsync("_ValidationScriptsPartial") } diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs b/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs index b0543f3..94f71f6 100644 --- a/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs @@ -7,6 +7,6 @@ { public Difficulty? SelectedDifficulty { get; set; } - public IReadOnlyCollection Recipes { get; set; } = new List(); + public List Recipes { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml deleted file mode 100644 index 565c6f6..0000000 --- a/Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml +++ /dev/null @@ -1,43 +0,0 @@ -@model IEnumerable - -
-

Zutaten

-
-
    - @foreach (var ingredient in Model) - { -
  • - - @ingredient.Ingredient.Name - @ingredient.Quantity @ingredient.Unit.Symbol -
  • - } -
- -
-
- -@section Scripts { - -} diff --git a/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml b/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml deleted file mode 100644 index 1efa149..0000000 --- a/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml +++ /dev/null @@ -1,56 +0,0 @@ -@model Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList -@using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList -@using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient -@using Francesco.Recipes.World.Models.BackendModels.Recipe - -@{ - ViewData["Title"] = "Einkaufsliste Details"; -} - -

Einkaufsliste Details

- -@if (TempData["SuccessMessage"] != null) -{ -
- @TempData["SuccessMessage"] -
-} - -
-

Einkaufsliste

-
-
-
- ID -
-
- @Model.Id -
-
-
- -

Rezepte

- - - - - - - - - @foreach (var recipeShoppingList in Model.RecipeShoppingList) - { - - - - - } - -
RezeptnameZutaten
@recipeShoppingList.Recipe.Name -
    - @foreach (var ingredient in recipeShoppingList.SelectedIngredients) - { -
  • @ingredient.RecipeIngredient.Ingredient.Name - @ingredient.RecipeIngredient.Quantity @ingredient.RecipeIngredient.Unit.Name
  • - } -
-
From d248ccb1dbe35ba99d2fb22a8b2ebcc503973484 Mon Sep 17 00:00:00 2001 From: franc Date: Tue, 8 Apr 2025 15:52:32 +0200 Subject: [PATCH 16/18] Reapply "add CategoryController with endpoints" This reverts commit 928743303c13a39ee1120cec3b27f7ad51055ce5. --- .../Controller/Category/CategoryController.cs | 3 +- .../MediaFile/MediaFileController.cs | 44 +- .../Controller/Recipe/RecipeController.cs | 160 ++++- .../ShoppingList/ShoppingListController.cs | 48 +- .../Data/FrancescosRecipesWorldDbContext.cs | 5 +- .../Francesco.Recipes.World.csproj | 10 +- ...120842_UpdateShoppingLIstLogic.Designer.cs | 572 ++++++++++++++++++ .../20250402120842_UpdateShoppingLIstLogic.cs | 162 +++++ ...escosRecipesWorldDbContextModelSnapshot.cs | 73 ++- .../Models/BackendModels/Recipe/Difficulty.cs | 9 +- .../RecipeIngredientShoppingList.cs | 6 +- .../RecipeShoppingList/RecipeShoppingList.cs | 17 + .../Shoppinglist/ShoppingList.cs | 4 +- Francesco.Recipes.World/Program.cs | 12 +- .../Category/CategoryRecipesViewModel.cs | 12 + .../Views/Recipe/AddInstruction.cshtml | 59 ++ .../Views/Recipe/AddOrCreateIngredient.cshtml | 36 +- .../Views/Recipe/CategoryRecipes.cshtml | 98 +++ .../Views/Recipe/Create.cshtml | 78 +-- .../Views/Recipe/Details.cshtml | 68 +++ .../Views/Recipe/FilterByDifficulty.cshtml | 100 +-- .../Recipe/FilterByDifficultyViewModel.cs | 2 +- ...AddIngredientsToShoppingListPartial.cshtml | 43 ++ .../Views/ShoppingList/Details.cshtml | 56 ++ 24 files changed, 1535 insertions(+), 142 deletions(-) create mode 100644 Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs create mode 100644 Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs create mode 100644 Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs create mode 100644 Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs create mode 100644 Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml create mode 100644 Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml create mode 100644 Francesco.Recipes.World/Views/Recipe/Details.cshtml create mode 100644 Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml create mode 100644 Francesco.Recipes.World/Views/ShoppingList/Details.cshtml diff --git a/Francesco.Recipes.World/Controller/Category/CategoryController.cs b/Francesco.Recipes.World/Controller/Category/CategoryController.cs index 0257ab4..29b06b5 100644 --- a/Francesco.Recipes.World/Controller/Category/CategoryController.cs +++ b/Francesco.Recipes.World/Controller/Category/CategoryController.cs @@ -5,6 +5,7 @@ using Francesco.Recipes.World.Repositories.Category; using Microsoft.AspNetCore.Mvc; + [ValidateAntiForgeryToken] [Route("Category")] public class CategoryController : Controller @@ -37,7 +38,7 @@ public async Task>> GetRecipesByCategory(Guid id) { var recipes = await _categoryRepository.GetRecipesByCategoryAsync(id); - return Ok(recipes); + return View(recipes); } } } diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs index 6cec16d..14f59fe 100644 --- a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -1,6 +1,48 @@ namespace Francesco.Recipes.World.Controller.MediaFile { - public class MediaFileController + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Repositories.MediaFile; + using Microsoft.AspNetCore.Mvc; + + [ValidateAntiForgeryToken] + + [Route("categories/{categoryId}/Recipe")] + public class MediaFileController : Controller { + private readonly IMediaFileRepository _mediaFileRepository; + private readonly FrancescosRecipesWorldDbContext _context; + + public MediaFileController(IMediaFileRepository mediaFileRepository, FrancescosRecipesWorldDbContext context) + { + _mediaFileRepository = mediaFileRepository; + _context = context; + } + + // POST: /UploadImage + [HttpPost("UploadImage")] + public async Task UploadImage(Guid recipeId, IFormFile? mediaFile) + { + if (mediaFile is null) + { + return BadRequest("Photo is required."); + } + + try + { + await _mediaFileRepository.UploadRecipeMediaAsync(recipeId, mediaFile); + return Ok("Image uploaded successfully."); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } + + // GET: /UploadImage + [HttpGet("UploadImage")] + public IActionResult UploadImageView() + { + return View(); + } } } diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 9b4cd9e..05d99aa 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -1,43 +1,94 @@ namespace Francesco.Recipes.World.Controller.Recipe { + using System.ComponentModel.DataAnnotations; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Repositories.Category; + using Francesco.Recipes.World.Repositories.Favorit; using Francesco.Recipes.World.Repositories.Ingredient; + using Francesco.Recipes.World.Repositories.Instruction; + using Francesco.Recipes.World.Repositories.MediaFile; using Francesco.Recipes.World.Repositories.Recipe; using Francesco.Recipes.World.Repositories.Unit; + using Francesco.Recipes.World.Views.Category; + using Francesco.Recipes.World.Views.Recipe; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; - [Route("categories/{categoryId}/Recipe")] + [ValidateAntiForgeryToken] + [Route("Recipe")] public class RecipeController : Controller { private readonly IRecipeRepository _recipeRepository; private readonly IUnitRepository _unitRepository; private readonly ICategoryRepository _categoryRepository; private readonly IIngredientRepository _ingredientRepository; + private readonly IMediaFileRepository _mediaFileRepository; + private readonly IInstructionRepository _instructionRepository; + private readonly IFavoritRepository _favoritRepository; - public RecipeController(IRecipeRepository recipeRepository, IUnitRepository unitRepository, ICategoryRepository categoryRepository, IIngredientRepository ingredientRepository) + [Display(Name = "Schwierigkeitsgrad")] + [BindProperty(SupportsGet = true)] + public Difficulty? SelectedDifficulty { get; set; } + + public RecipeController(IRecipeRepository recipeRepository, IUnitRepository unitRepository, ICategoryRepository categoryRepository, IIngredientRepository ingredientRepository, IMediaFileRepository mediaFileRepository, IInstructionRepository instructionRepository, IFavoritRepository favoritRepository) { _recipeRepository = recipeRepository; _unitRepository = unitRepository; _categoryRepository = categoryRepository; _ingredientRepository = ingredientRepository; + Recipes = new List(); + _mediaFileRepository = mediaFileRepository; + _instructionRepository = instructionRepository; + _favoritRepository = favoritRepository; } public IReadOnlyCollection Recipes { get; set; } - // GET: /Recipe/AddOrCreateIngredient + // GET: /Recipe/{recipeId}/AddOrCreateIngredient [HttpGet("{recipeId}/AddOrCreateIngredient")] - public async Task AddOrCreateIngredient() + public async Task AddOrCreateIngredient(Guid recipeId) { var units = await _unitRepository.GetAllUnitsAsync(); ViewBag.Units = new SelectList(units, "Id", "Name"); + ViewBag.RecipeId = recipeId; return View(); } - // POST: /Recipe/AddOrCreateIngredient + // GET: /Recipe/Details/{recipeId} + [HttpGet("Details/{recipeId}")] + public async Task Details(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return View(recipe); + } + + // GET: /Recipe/CategoryRecipes + [HttpGet("CategoryRecipes")] + public async Task CategoryRecipes() + { + var categories = await _categoryRepository.GetAllCategoriesAsync(); + var viewModel = new List(); + + foreach (var category in categories) + { + var recipes = await _categoryRepository.GetRecipesByCategoryAsync(category.Id); + viewModel.Add(new CategoryRecipesViewModel + { + Category = category, + Recipes = recipes, + }); + } + + return View(viewModel); + } + + // POST: /Recipe/{recipeId}/AddOrCreateIngredient [HttpPost("{recipeId}/AddOrCreateIngredient")] - [ValidateAntiForgeryToken] public async Task AddOrCreateIngredient(Guid recipeId, string ingredientName, int quantity, Guid unitId) { if (quantity <= 0) @@ -56,8 +107,8 @@ return RedirectToAction("Details", new { id = recipeId }); } - // GET: /categories/{categoryId}/Recipe/Create - [HttpGet("Create")] + // GET: /Recipe/Create/{categoryId} + [HttpGet("Create/{categoryId}")] public async Task Create(Guid categoryId) { var category = await _categoryRepository.GetCategoryByIdAsync(categoryId); @@ -70,10 +121,9 @@ return View(); } - // POST: /categories/{categoryId}/Recipe/Create - [HttpPost("Create")] - [ValidateAntiForgeryToken] - public async Task Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime) + // POST: /Recipe/Create/{categoryId} + [HttpPost("Create/{categoryId}")] + public async Task Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime, IFormFile? photo) { if (string.IsNullOrWhiteSpace(name)) { @@ -103,13 +153,18 @@ return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } - await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); + var recipe = await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); + if (photo != null) + { + await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, photo); + } + return RedirectToAction("Details", "Category", new { id = categoryId }); } - // GET: /categories/{categoryId}/Recipe/{recipeId}/RemoveIngredient/{ingredientId} + // GET: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} [HttpGet("{recipeId}/RemoveIngredient/{ingredientId}")] - public async Task RemoveIngredient(Guid categoryId, Guid recipeId, Guid ingredientId) + public async Task RemoveIngredient(Guid recipeId, Guid ingredientId) { var recipe = await _recipeRepository.GetRecipeAsync(recipeId); var ingredient = await _ingredientRepository.GetIngredientByIdAsync(ingredientId); @@ -121,28 +176,87 @@ ViewBag.RecipeId = recipeId; ViewBag.IngredientId = ingredientId; - ViewBag.CategoryId = categoryId; ViewBag.IngredientName = ingredient.Name; return View(); } - // POST: /categories/{categoryId}/Recipe/{recipeId}/RemoveIngredient/{ingredientId} + // POST: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} [HttpPost("{recipeId}/RemoveIngredient/{ingredientId}")] - [ValidateAntiForgeryToken] - public async Task RemoveIngredientConfirmed(Guid categoryId, Guid recipeId, Guid ingredientId) + public async Task RemoveIngredientConfirmed(Guid recipeId, Guid ingredientId) { await _recipeRepository.RemoveIngredientFromRecipeAsync(recipeId, ingredientId); TempData["SuccessMessage"] = "Ingredient removed successfully."; return RedirectToAction("Details", new { id = recipeId }); } - // GET: /categories/{categoryId}/Recipe/FilterByDifficulty + // GET: /Recipe/FilterByDifficulty [HttpGet("FilterByDifficulty")] - public async Task FilterByDifficulty(Difficulty? difficulty) + public async Task FilterByDifficulty(Difficulty? selectedDifficulty) { - Recipes = await _recipeRepository.GetRecipesByDifficultyAsync(difficulty); - return View(Recipes); + var recipes = await _recipeRepository.GetRecipesByDifficultyAsync(selectedDifficulty); + var viewModel = new FilterByDifficultyViewModel + { + SelectedDifficulty = selectedDifficulty, + Recipes = recipes, + }; + return View(viewModel); + } + + // GET: /Recipe/{recipeId}/AddInstruction + [HttpGet("{recipeId}/AddInstruction")] + public async Task AddInstruction(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + ViewBag.RecipeId = recipeId; + return View(recipe); + } + + // POST: /Recipe/{recipeId}/AddInstruction + [HttpPost("{recipeId}/AddInstruction")] + public async Task AddInstruction(Guid recipeId, string description, int number) + { + try + { + await _instructionRepository.CreateInstructionToRecipeAsync(recipeId, description, number); + return RedirectToAction("AddInstruction", new { recipeId }); + } + catch (Exception ex) + { + ModelState.AddModelError(string.Empty, ex.Message); + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + ViewBag.RecipeId = recipeId; + return View(recipe); + } + } + + // GET: /Recipe/Favorites + [HttpGet("Favorites")] + public async Task Favorites() + { + var favoriteRecipes = await _favoritRepository.GetFavoriteRecipesAsync(); + return View(favoriteRecipes); + } + + // POST: /Recipe/AddFavorite + [HttpPost("AddFavorite")] + public async Task AddFavorite(Guid recipeId) + { + await _favoritRepository.AddFavoriteAsync(recipeId); + return RedirectToAction("Details", new { recipeId }); + } + + // POST: /Recipe/RemoveFavorite + [HttpPost("RemoveFavorite")] + public async Task RemoveFavorite(Guid recipeId) + { + await _favoritRepository.RemoveFavoriteAsync(recipeId); + return RedirectToAction("Details", new { recipeId }); } } } diff --git a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs index 38fdf4b..7f6a434 100644 --- a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs +++ b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs @@ -1,6 +1,50 @@ -namespace Francesco.Recipes.World.Controller.ShoppingList +namespace Francesco.Recipes.World.Controllers { - public class ShoppingListController + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Repositories.ShoppingList; + using Microsoft.AspNetCore.Mvc; + using Microsoft.EntityFrameworkCore; + + [Route("ShoppingList")] + public class ShoppingListController : Controller { + private readonly IShoppingListRepository _shoppingListRepository; + private readonly FrancescosRecipesWorldDbContext _context; + + public ShoppingListController(IShoppingListRepository shoppingListRepository, FrancescosRecipesWorldDbContext context) + { + _shoppingListRepository = shoppingListRepository; + _context = context; + } + + [HttpPost("CreateOrAddIngredients")] + public async Task CreateOrAddIngredients([FromBody] CreateOrAddIngredientsRequest request) + { + if (request == null || request.IngredientIds == null || !request.IngredientIds.Any()) + { + return BadRequest("No ingredients provided."); + } + + await _shoppingListRepository.AddIngredientsToShoppingListAsync(request.RecipeId, request.IngredientIds); + + var shoppingList = await _context.ShoppingLists + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.Recipe) + .FirstOrDefaultAsync(sl => sl.RecipeShoppingList.Any(rsl => rsl.Recipe.Id == request.RecipeId)); + + if (shoppingList == null) + { + return BadRequest("Error creating shopping list."); + } + + return Json(new { shoppingListId = shoppingList.Id }); + } + + public class CreateOrAddIngredientsRequest + { + public Guid RecipeId { get; set; } + + public List IngredientIds { get; set; } = new (); + } } } diff --git a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs index 4fd6bd8..7a8a5e7 100644 --- a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs +++ b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs @@ -9,7 +9,8 @@ using Francesco.Recipes.World.Models.BackendModels.MediaFile; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; - using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; +using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; +using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; using Francesco.Recipes.World.Models.BackendModels.Unit; using Microsoft.EntityFrameworkCore; @@ -36,6 +37,8 @@ public DbSet RecipeIngredientsShoppingLists => Set(); + public DbSet RecipeShoppingLists => Set(); + public DbSet ShoppingLists => Set(); public DbSet MediaFiles => Set(); diff --git a/Francesco.Recipes.World/Francesco.Recipes.World.csproj b/Francesco.Recipes.World/Francesco.Recipes.World.csproj index 549a91d..7d430a2 100644 --- a/Francesco.Recipes.World/Francesco.Recipes.World.csproj +++ b/Francesco.Recipes.World/Francesco.Recipes.World.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -45,5 +45,13 @@ + + + + + + + + diff --git a/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs new file mode 100644 index 0000000..d99ae26 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.Designer.cs @@ -0,0 +1,572 @@ +// + +using Francesco.Recipes.World.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + [DbContext(typeof(FrancescosRecipesWorldDbContext))] + [Migration("20250402120842_UpdateShoppingLIstLogic")] + partial class UpdateShoppingLIstLogic + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new + { + Id = new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"), + Name = "Vorspeisen & Snacks" + }, + new + { + Id = new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), + Name = "Erste Gänge" + }, + new + { + Id = new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), + Name = "Hauptgerichte" + }, + new + { + Id = new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"), + Name = "Desserts & Süßspeisen" + }, + new + { + Id = new Guid("28e39168-701a-4084-81da-d96c987c462f"), + Name = "Beilagen & Salate" + }, + new + { + Id = new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"), + Name = "Kuchen" + }, + new + { + Id = new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), + Name = "Hefegebäck & Brot" + }, + new + { + Id = new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"), + Name = "Soßen & Saucen" + }, + new + { + Id = new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), + Name = "Marmeladen & Eingemachtes" + }, + new + { + Id = new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), + Name = "Getränke" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Favorits"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsChecked") + .HasColumnType("bit"); + + b.Property("RecipeIngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeIngredientId"); + + b.HasIndex("RecipeShoppingListId"); + + b.ToTable("RecipeIngredientsShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.ToTable("Instructions"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Data") + .HasColumnType("varbinary(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("InstructionId") + .HasColumnType("uniqueidentifier"); + + b.Property("MimeType") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("InstructionId"); + + b.HasIndex("RecipeId"); + + b.ToTable("MediaFiles"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CookingTime") + .HasColumnType("time"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Difficulty") + .HasColumnType("int"); + + b.Property("FavoritId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsFavorite") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PreparationTime") + .HasColumnType("time"); + + b.Property("Servings") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("FavoritId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UnitId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeId"); + + b.HasIndex("UnitId"); + + b.ToTable("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.HasIndex("ShoppingListId"); + + b.ToTable("RecipeShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("ShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Units"); + + b.HasData( + new + { + Id = new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), + Name = "liter", + Symbol = "l" + }, + new + { + Id = new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), + Name = "gramm", + Symbol = "g" + }, + new + { + Id = new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), + Name = "kilogramm", + Symbol = "kg" + }, + new + { + Id = new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), + Name = "stücke", + Symbol = "stk" + }, + new + { + Id = new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), + Name = "blatt", + Symbol = "blatt" + }, + new + { + Id = new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), + Name = "messerspitze", + Symbol = "msp" + }, + new + { + Id = new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), + Name = "stange", + Symbol = "stange" + }, + new + { + Id = new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"), + Name = "bund", + Symbol = "bund" + }, + new + { + Id = new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), + Name = "zehe", + Symbol = "zehe" + }, + new + { + Id = new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), + Name = "teelöffel", + Symbol = "TL" + }, + new + { + Id = new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), + Name = "esslöffel", + Symbol = "EL" + }); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", null) + .WithMany("IngredientShoppingLists") + .HasForeignKey("IngredientId"); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", "RecipeIngredient") + .WithMany() + .HasForeignKey("RecipeIngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList") + .WithMany("SelectedIngredients") + .HasForeignKey("RecipeShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RecipeIngredient"); + + b.Navigation("RecipeShoppingList"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("Instructions") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.MediaFile.MediaFile", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", "Instruction") + .WithMany("MediaFiles") + .HasForeignKey("InstructionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("MediaFiles") + .HasForeignKey("RecipeId"); + + b.Navigation("Instruction"); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category") + .WithMany("Recipes") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit") + .WithMany("Recipe") + .HasForeignKey("FavoritId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Favorit"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient") + .WithMany("RecipeIngredients") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany("RecipeIngredients") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit") + .WithMany("RecipeIngredient") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("Recipe"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany() + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") + .WithMany("RecipeIngredientShoppingLists") + .HasForeignKey("ShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + + b.Navigation("ShoppingList"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => + { + b.Navigation("Recipes"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => + { + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => + { + b.Navigation("IngredientShoppingLists"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => + { + b.Navigation("MediaFiles"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + { + b.Navigation("Instructions"); + + b.Navigation("MediaFiles"); + + b.Navigation("RecipeIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Navigation("SelectedIngredients"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => + { + b.Navigation("RecipeIngredientShoppingLists"); + }); + + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => + { + b.Navigation("RecipeIngredient"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs new file mode 100644 index 0000000..5caf69f --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250402120842_UpdateShoppingLIstLogic.cs @@ -0,0 +1,162 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + /// + public partial class UpdateShoppingLIstLogic : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId", + table: "RecipeIngredientsShoppingLists"); + + migrationBuilder.DropForeignKey( + name: "FK_Recipes_Categories_CategoryId", + table: "Recipes"); + + migrationBuilder.RenameColumn( + name: "ShoppingListId", + table: "RecipeIngredientsShoppingLists", + newName: "RecipeShoppingListId"); + + migrationBuilder.RenameIndex( + name: "IX_RecipeIngredientsShoppingLists_ShoppingListId", + table: "RecipeIngredientsShoppingLists", + newName: "IX_RecipeIngredientsShoppingLists_RecipeShoppingListId"); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Recipes", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "IsChecked", + table: "RecipeIngredientsShoppingLists", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "RecipeShoppingLists", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ShoppingListId = table.Column(type: "uniqueidentifier", nullable: false), + RecipeId = table.Column(type: "uniqueidentifier", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_RecipeShoppingLists", x => x.Id); + table.ForeignKey( + name: "FK_RecipeShoppingLists_Recipes_RecipeId", + column: x => x.RecipeId, + principalTable: "Recipes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RecipeShoppingLists_ShoppingLists_ShoppingListId", + column: x => x.ShoppingListId, + principalTable: "ShoppingLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RecipeShoppingLists_RecipeId", + table: "RecipeShoppingLists", + column: "RecipeId"); + + migrationBuilder.CreateIndex( + name: "IX_RecipeShoppingLists_ShoppingListId", + table: "RecipeShoppingLists", + column: "ShoppingListId"); + + migrationBuilder.AddForeignKey( + name: "FK_RecipeIngredientsShoppingLists_RecipeShoppingLists_RecipeShoppingListId", + table: "RecipeIngredientsShoppingLists", + column: "RecipeShoppingListId", + principalTable: "RecipeShoppingLists", + principalColumn: "Id", + onDelete: ReferentialAction.NoAction); + + migrationBuilder.AddForeignKey( + name: "FK_Recipes_Categories_CategoryId", + table: "Recipes", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_RecipeIngredientsShoppingLists_RecipeShoppingLists_RecipeShoppingListId", + table: "RecipeIngredientsShoppingLists"); + + migrationBuilder.DropForeignKey( + name: "FK_Recipes_Categories_CategoryId", + table: "Recipes"); + + migrationBuilder.DropTable( + name: "RecipeShoppingLists"); + + migrationBuilder.DropColumn( + name: "IsChecked", + table: "RecipeIngredientsShoppingLists"); + + migrationBuilder.RenameColumn( + name: "RecipeShoppingListId", + table: "RecipeIngredientsShoppingLists", + newName: "ShoppingListId"); + + migrationBuilder.RenameIndex( + name: "IX_RecipeIngredientsShoppingLists_RecipeShoppingListId", + table: "RecipeIngredientsShoppingLists", + newName: "IX_RecipeIngredientsShoppingLists_ShoppingListId"); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Recipes", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + + migrationBuilder.AddForeignKey( + name: "FK_RecipeIngredientsShoppingLists_ShoppingLists_ShoppingListId", + table: "RecipeIngredientsShoppingLists", + column: "ShoppingListId", + principalTable: "ShoppingLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Recipes_Categories_CategoryId", + table: "Recipes", + column: "CategoryId", + principalTable: "Categories", + principalColumn: "Id"); + } + } +} diff --git a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs index 6b432d8..c27540a 100644 --- a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs +++ b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs @@ -1,7 +1,10 @@ // +using System; using Francesco.Recipes.World.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable @@ -124,10 +127,13 @@ namespace Francesco.Recipes.World.Migrations b.Property("IngredientId") .HasColumnType("uniqueidentifier"); + b.Property("IsChecked") + .HasColumnType("bit"); + b.Property("RecipeIngredientId") .HasColumnType("uniqueidentifier"); - b.Property("ShoppingListId") + b.Property("RecipeShoppingListId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); @@ -136,7 +142,7 @@ namespace Francesco.Recipes.World.Migrations b.HasIndex("RecipeIngredientId"); - b.HasIndex("ShoppingListId"); + b.HasIndex("RecipeShoppingListId"); b.ToTable("RecipeIngredientsShoppingLists"); }); @@ -200,7 +206,7 @@ namespace Francesco.Recipes.World.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("CategoryId") + b.Property("CategoryId") .HasColumnType("uniqueidentifier"); b.Property("CookingTime") @@ -272,6 +278,27 @@ namespace Francesco.Recipes.World.Migrations b.ToTable("RecipeIngredients"); }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShoppingListId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RecipeId"); + + b.HasIndex("ShoppingListId"); + + b.ToTable("RecipeShoppingLists"); + }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => { b.Property("Id") @@ -388,15 +415,15 @@ namespace Francesco.Recipes.World.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") - .WithMany("RecipeIngredientShoppingLists") - .HasForeignKey("ShoppingListId") + b.HasOne("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList") + .WithMany("SelectedIngredients") + .HasForeignKey("RecipeShoppingListId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("RecipeIngredient"); - b.Navigation("ShoppingList"); + b.Navigation("RecipeShoppingList"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => @@ -429,9 +456,11 @@ namespace Francesco.Recipes.World.Migrations modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", null) + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", "Category") .WithMany("Recipes") - .HasForeignKey("CategoryId"); + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.HasOne("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", "Favorit") .WithMany("Recipe") @@ -439,6 +468,8 @@ namespace Francesco.Recipes.World.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Category"); + b.Navigation("Favorit"); }); @@ -469,6 +500,25 @@ namespace Francesco.Recipes.World.Migrations b.Navigation("Unit"); }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", "Recipe") + .WithMany() + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "ShoppingList") + .WithMany("RecipeIngredientShoppingLists") + .HasForeignKey("ShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + + b.Navigation("ShoppingList"); + }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => { b.Navigation("Recipes"); @@ -500,6 +550,11 @@ namespace Francesco.Recipes.World.Migrations b.Navigation("RecipeIngredients"); }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList.RecipeShoppingList", b => + { + b.Navigation("SelectedIngredients"); + }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", b => { b.Navigation("RecipeIngredientShoppingLists"); diff --git a/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs index ee81fd2..5dc055b 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs @@ -1,15 +1,22 @@ -namespace Francesco.Recipes.World.Models.BackendModels.Recipe +using System.ComponentModel.DataAnnotations; + +namespace Francesco.Recipes.World.Models.BackendModels.Recipe { public enum Difficulty { + [Display(Name = "Sehr einfach")] VeryEasy = 0, + [Display(Name = "Einfach")] Easy = 1, + [Display(Name = "Mittel")] Medium = 2, + [Display(Name = "Schwer")] Hard = 3, + [Display(Name = "Experte")] Expert = 4, } } diff --git a/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs index 1878caf..fa691b8 100644 --- a/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs +++ b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs @@ -1,14 +1,16 @@ namespace Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList { using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; - using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; + using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; public class RecipeIngredientShoppingList { public Guid Id { get; set; } - public virtual ShoppingList ShoppingList { get; set; } = new (); + public virtual RecipeShoppingList RecipeShoppingList { get; set; } = new (); public virtual RecipeIngredient RecipeIngredient { get; set; } = new (); + + public bool IsChecked { get; set; } = false; } } diff --git a/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs new file mode 100644 index 0000000..1173425 --- /dev/null +++ b/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs @@ -0,0 +1,17 @@ +namespace Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList +{ + using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; + + public class RecipeShoppingList + { + public Guid Id { get; set; } + + public virtual ShoppingList ShoppingList { get; set; } = new ShoppingList(); + + public virtual Recipe Recipe { get; set; } = new (); + + public virtual ICollection SelectedIngredients { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs index c8fd12f..1a06a05 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs @@ -1,6 +1,6 @@ namespace Francesco.Recipes.World.Models.BackendModels.Shoppinglist { - using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; + using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; public class ShoppingList : ITimeStampedEntity { @@ -10,6 +10,6 @@ public DateTime? ModifiedAt { get; set; } - public virtual ICollection RecipeIngredientShoppingLists { get; set; } = new List(); + public virtual ICollection RecipeShoppingList { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index 719acda..6f98ff1 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -1,5 +1,6 @@ using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Repositories.Category; +using Francesco.Recipes.World.Repositories.Favorit; using Francesco.Recipes.World.Repositories.Ingredient; using Francesco.Recipes.World.Repositories.Instruction; using Francesco.Recipes.World.Repositories.MediaFile; @@ -7,8 +8,6 @@ using Francesco.Recipes.World.Repositories.Recipe; using Francesco.Recipes.World.Repositories.ShoppingList; using Francesco.Recipes.World.Repositories.Unit; -using Microsoft.AspNetCore.Identity; - using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -23,9 +22,6 @@ var connectionString = builder.Configuration.GetConnectionString("FrancescosReci services.AddDbContext(options => options.UseSqlServer(connectionString)); -services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) - .AddEntityFrameworkStores(); - // Add services to the container. builder.Services.AddControllersWithViews(); @@ -43,6 +39,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -62,10 +60,6 @@ app.UseStaticFiles(); app.UseRouting(); -app.UseAuthentication(); - -app.UseAuthorization(); - app.MapDefaultControllerRoute(); app.MapControllerRoute( diff --git a/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs b/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs new file mode 100644 index 0000000..1559dd8 --- /dev/null +++ b/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs @@ -0,0 +1,12 @@ +namespace Francesco.Recipes.World.Views.Category +{ + using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + + public class CategoryRecipesViewModel + { + public Category Category { get; set; } = new (); + + public IEnumerable Recipes { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml b/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml new file mode 100644 index 0000000..d6d5f79 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml @@ -0,0 +1,59 @@ +@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe + +@{ + ViewData["Title"] = "Add Instructions"; +} + +

Add Instructions to @Model.Name

+ +
+

Existing Instructions

+
    + @foreach (var instruction in Model.Instructions.OrderBy(i => i.Number)) + { +
  • @instruction.Number. @instruction.Description
  • + } +
+
+ +
+

Add New Instruction

+
+ +
+ + +
+
+ + +
+ +
+
+ +@section Scripts { + +} + diff --git a/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml index c72fdcf..1232469 100644 --- a/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml @@ -1,29 +1,27 @@ @{ - ViewData["Title"] = "Add or Create Ingredient to Recipe"; + ViewData["Title"] = "Add or Create Ingredient to Recipe"; }

@ViewData["Title"]

-
- - -
-
- - -
-
- - -
-
- - -
- + +
+ + +
+
+ + +
+
+ + +
+
@section Scripts { - + } + diff --git a/Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml b/Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml new file mode 100644 index 0000000..df06c6e --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/CategoryRecipes.cshtml @@ -0,0 +1,98 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Category Recipes"; +} + +

Category Recipes

+ +@foreach (var categoryRecipes in Model) +{ +
+

@categoryRecipes.Category.Name

+ Rezept erstellen +
+ @foreach (var recipe in categoryRecipes.Recipes) + { +
+
+ @if (recipe.MediaFiles.Any() && recipe.MediaFiles.First().Data != null) + { + var mediaFile = recipe.MediaFiles.First(); + if (mediaFile.Data != null) + { + @recipe.Name + } + } +
+
+

@recipe.Name

+

@recipe.Description

+

Difficulty: @recipe.Difficulty

+

Servings: @recipe.Servings

+

Preparation Time: @recipe.PreparationTime

+

Cooking Time: @recipe.CookingTime

+
+ @if (recipe.IsFavorite) + { +
+ + +
+ } + else + { +
+ + +
+ } +
+
+
+ } + +
+
+} + + + diff --git a/Francesco.Recipes.World/Views/Recipe/Create.cshtml b/Francesco.Recipes.World/Views/Recipe/Create.cshtml index 8b1e201..e8c3769 100644 --- a/Francesco.Recipes.World/Views/Recipe/Create.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/Create.cshtml @@ -1,45 +1,53 @@ @model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe @using Francesco.Recipes.World.Models.BackendModels.Recipe @{ - ViewData["Title"] = "Create Recipe"; + ViewData["Title"] = "Create Recipe"; }

Create Recipe

-
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- -
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
@section Scripts { diff --git a/Francesco.Recipes.World/Views/Recipe/Details.cshtml b/Francesco.Recipes.World/Views/Recipe/Details.cshtml new file mode 100644 index 0000000..4ea25c7 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/Details.cshtml @@ -0,0 +1,68 @@ +@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe + +@{ + ViewData["Title"] = "Recipe Details"; +} + +

@Model.Name

+ +
+
+ @if (Model.MediaFiles.Any() && Model.MediaFiles.First().Data != null) + { + var mediaFile = Model.MediaFiles.First(); + if (mediaFile.Data != null) + { + @Model.Name + } + } +
+
+

Description: @Model.Description

+

Difficulty: @Model.Difficulty

+

Servings: @Model.Servings

+

Preparation Time: @Model.PreparationTime

+

Cooking Time: @Model.CookingTime

+
+
+

Ingredients

+
+
    + @foreach (var ingredient in Model.RecipeIngredients) + { +
  • + + @ingredient.Ingredient.Name - @ingredient.Quantity @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty) +
  • + } +
+ +
+
+
+ +@section Scripts { + +} diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml index 01a77cb..b0e499e 100644 --- a/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml @@ -1,42 +1,72 @@ - @model Francesco.Recipes.World.Views.Recipe.FilterByDifficultyViewModel - @using Francesco.Recipes.World.Models.BackendModels.Recipe +@using Francesco.Recipes.World.Models.BackendModels.Recipe +@model Francesco.Recipes.World.Views.Recipe.FilterByDifficultyViewModel -@{ - ViewData["Title"] = "Filter Recipes by Difficulty"; -} +

Rezepte nach Schwierigkeitsgrad

-

Filter Recipes by Difficulty

+ -
-
- - - -
-
+
+
+
+
+ + +
+
-@if (Model.Recipes != null && Model.Recipes.Any()) -{ -

Filtered Recipes

-
    - @foreach (var recipe in Model.Recipes) - { -
  • @recipe.Name - @recipe.Difficulty.
  • - } -
-} -else -{ -

No recipes found for the selected difficulty.

-} +
+
+ +
+ @if (Model?.Recipes != null && Model.Recipes.Any()) + { +
+ + + + + + + + + + + + + @foreach (var recipe in Model.Recipes) + { + + + + + + + + + } + +
NameBeschreibungSchwierigkeitsgradPortionenZubereitungszeitAktionen
@recipe.Name@(recipe.Description?.Length > 100 ? recipe.Description.Substring(0, 100) + "..." : recipe.Description)@recipe.?Difficulty@recipe.Servings@($"{recipe.PreparationTime.TotalMinutes} Min.") + Details + Bearbeiten +
+
+ } + else + { +
+

Keine Rezepte gefunden.

+
+ } +
@section Scripts { - - @await Html.PartialAsync("_ValidationScriptsPartial") + } diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs b/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs index 94f71f6..b0543f3 100644 --- a/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs @@ -7,6 +7,6 @@ { public Difficulty? SelectedDifficulty { get; set; } - public List Recipes { get; set; } = new List(); + public IReadOnlyCollection Recipes { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml new file mode 100644 index 0000000..565c6f6 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_AddIngredientsToShoppingListPartial.cshtml @@ -0,0 +1,43 @@ +@model IEnumerable + +
+

Zutaten

+
+
    + @foreach (var ingredient in Model) + { +
  • + + @ingredient.Ingredient.Name - @ingredient.Quantity @ingredient.Unit.Symbol +
  • + } +
+ +
+
+ +@section Scripts { + +} diff --git a/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml b/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml new file mode 100644 index 0000000..1efa149 --- /dev/null +++ b/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml @@ -0,0 +1,56 @@ +@model Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList +@using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList +@using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient +@using Francesco.Recipes.World.Models.BackendModels.Recipe + +@{ + ViewData["Title"] = "Einkaufsliste Details"; +} + +

Einkaufsliste Details

+ +@if (TempData["SuccessMessage"] != null) +{ +
+ @TempData["SuccessMessage"] +
+} + +
+

Einkaufsliste

+
+
+
+ ID +
+
+ @Model.Id +
+
+
+ +

Rezepte

+ + + + + + + + + @foreach (var recipeShoppingList in Model.RecipeShoppingList) + { + + + + + } + +
RezeptnameZutaten
@recipeShoppingList.Recipe.Name +
    + @foreach (var ingredient in recipeShoppingList.SelectedIngredients) + { +
  • @ingredient.RecipeIngredient.Ingredient.Name - @ingredient.RecipeIngredient.Quantity @ingredient.RecipeIngredient.Unit.Name
  • + } +
+
From 127a60b6217a198b264ec16ec8a5beff2b9147d2 Mon Sep 17 00:00:00 2001 From: franc Date: Wed, 9 Apr 2025 15:37:44 +0200 Subject: [PATCH 17/18] Refactoring some Code and clean Code --- .../Controller/Category/CategoryController.cs | 17 ++---- .../MediaFile/MediaFileController.cs | 32 +++++++++- .../Controller/Recipe/RecipeController.cs | 51 ++++++++-------- .../ShoppingList/ShoppingListController.cs | 12 +--- .../RecipeShoppingList/RecipeShoppingList.cs | 2 +- .../CreateOrAddIngredientRequestModel.cs | 9 +++ Francesco.Recipes.World/Program.cs | 4 +- .../Category/CategoryRepository.cs | 7 +++ .../Category/ICategoryRepository.cs | 2 + .../Repositories/Favorit/FavoritRepository.cs | 2 +- ...itRepository.cs => IFavoriteRepository.cs} | 2 +- .../Ingredient/IngredientRepository.cs | 2 - .../Instruction/IInstructionRepository.cs | 3 +- .../Instruction/InstructionRepository.cs | 29 +++++---- .../MediaFile/IMediaFileRepository.cs | 2 +- .../MediaFile/MediaFileRepository.cs | 60 +++++++------------ .../Repositories/Recipe/IRecipeRepository.cs | 2 +- .../Repositories/Recipe/RecipeRepository.cs | 6 +- .../ShoppingList/ShoppingListRepository.cs | 2 - 19 files changed, 132 insertions(+), 114 deletions(-) create mode 100644 Francesco.Recipes.World/Models/CreateOrAddIngredientRequestModel.cs rename Francesco.Recipes.World/Repositories/Favorit/{IFavoritRepository.cs => IFavoriteRepository.cs} (89%) diff --git a/Francesco.Recipes.World/Controller/Category/CategoryController.cs b/Francesco.Recipes.World/Controller/Category/CategoryController.cs index 29b06b5..f077af7 100644 --- a/Francesco.Recipes.World/Controller/Category/CategoryController.cs +++ b/Francesco.Recipes.World/Controller/Category/CategoryController.cs @@ -1,13 +1,8 @@ namespace Francesco.Recipes.World.Controller.Category { - using Francesco.Recipes.World.Models.BackendModels.Category; - using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Repositories.Category; using Microsoft.AspNetCore.Mvc; - [ValidateAntiForgeryToken] - [Route("Category")] - public class CategoryController : Controller { private readonly ICategoryRepository _categoryRepository; @@ -19,7 +14,7 @@ // GET: /Category [HttpGet] - public async Task>> Index() + public async Task Index() { var categories = await _categoryRepository.GetAllCategoriesAsync(); return View(categories); @@ -29,16 +24,16 @@ [HttpGet("{id:guid}")] public async Task Details(Guid id) { - var category = await _categoryRepository.GetCategoryByIdAsync(id); - return View(category); + var category = await _categoryRepository.GetCategoryByIdAsync(id); + return View(category); } // GET: /Category/{id}/recipes [HttpGet("{id:guid}/recipes")] - public async Task>> GetRecipesByCategory(Guid id) + public async Task GetRecipesByCategory(Guid id) { - var recipes = await _categoryRepository.GetRecipesByCategoryAsync(id); - return View(recipes); + var recipes = await _categoryRepository.GetRecipesByCategoryAsync(id); + return Ok(recipes); } } } diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs index 14f59fe..f021a8f 100644 --- a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -4,9 +4,7 @@ using Francesco.Recipes.World.Repositories.MediaFile; using Microsoft.AspNetCore.Mvc; - [ValidateAntiForgeryToken] - - [Route("categories/{categoryId}/Recipe")] + [Route("Category/{categoryId}/Recipe")] public class MediaFileController : Controller { private readonly IMediaFileRepository _mediaFileRepository; @@ -20,6 +18,7 @@ // POST: /UploadImage [HttpPost("UploadImage")] + [AutoValidateAntiforgeryToken] public async Task UploadImage(Guid recipeId, IFormFile? mediaFile) { if (mediaFile is null) @@ -44,5 +43,32 @@ { return View(); } + + [HttpPost("ReplaceInstructionImage")] + [ValidateAntiForgeryToken] + public async Task ReplaceInstructionImage(Guid instructionId, Guid mediaFileIdToReplace, IFormFile? newPhoto) + { + if (newPhoto is null) + { + return BadRequest("Photo is required."); + } + + using (var memoryStream = new MemoryStream()) + { + await newPhoto.CopyToAsync(memoryStream); + + var newMediaData = memoryStream.ToArray(); + + try + { + await _mediaFileRepository.ReplaceInstructionImageAsync(instructionId, mediaFileIdToReplace, newPhoto.FileName, newPhoto.ContentType, newMediaData); + return Ok("Image replaced successfully."); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } + } } } diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 05d99aa..b86ef61 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -1,6 +1,5 @@ namespace Francesco.Recipes.World.Controller.Recipe { - using System.ComponentModel.DataAnnotations; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Repositories.Category; using Francesco.Recipes.World.Repositories.Favorit; @@ -14,8 +13,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; - [ValidateAntiForgeryToken] - [Route("Recipe")] public class RecipeController : Controller { private readonly IRecipeRepository _recipeRepository; @@ -24,13 +21,18 @@ private readonly IIngredientRepository _ingredientRepository; private readonly IMediaFileRepository _mediaFileRepository; private readonly IInstructionRepository _instructionRepository; - private readonly IFavoritRepository _favoritRepository; + private readonly IFavoriteRepository _favoriteRepository; - [Display(Name = "Schwierigkeitsgrad")] - [BindProperty(SupportsGet = true)] - public Difficulty? SelectedDifficulty { get; set; } + public IReadOnlyCollection Recipes { get; set; } - public RecipeController(IRecipeRepository recipeRepository, IUnitRepository unitRepository, ICategoryRepository categoryRepository, IIngredientRepository ingredientRepository, IMediaFileRepository mediaFileRepository, IInstructionRepository instructionRepository, IFavoritRepository favoritRepository) + public RecipeController( + IRecipeRepository recipeRepository, + IUnitRepository unitRepository, + ICategoryRepository categoryRepository, + IIngredientRepository ingredientRepository, + IMediaFileRepository mediaFileRepository, + IInstructionRepository instructionRepository, + IFavoriteRepository favoriteRepository) { _recipeRepository = recipeRepository; _unitRepository = unitRepository; @@ -39,11 +41,9 @@ Recipes = new List(); _mediaFileRepository = mediaFileRepository; _instructionRepository = instructionRepository; - _favoritRepository = favoritRepository; + _favoriteRepository = favoriteRepository; } - public IReadOnlyCollection Recipes { get; set; } - // GET: /Recipe/{recipeId}/AddOrCreateIngredient [HttpGet("{recipeId}/AddOrCreateIngredient")] public async Task AddOrCreateIngredient(Guid recipeId) @@ -71,24 +71,20 @@ [HttpGet("CategoryRecipes")] public async Task CategoryRecipes() { - var categories = await _categoryRepository.GetAllCategoriesAsync(); - var viewModel = new List(); + var categories = await _categoryRepository.GetAllCategoriesWithRecipesAsync(); - foreach (var category in categories) + var viewModel = categories.Select(c => new CategoryRecipesViewModel { - var recipes = await _categoryRepository.GetRecipesByCategoryAsync(category.Id); - viewModel.Add(new CategoryRecipesViewModel - { - Category = category, - Recipes = recipes, - }); - } + Category = c, + Recipes = c.Recipes, + }).ToList(); return View(viewModel); } // POST: /Recipe/{recipeId}/AddOrCreateIngredient [HttpPost("{recipeId}/AddOrCreateIngredient")] + [ValidateAntiForgeryToken] public async Task AddOrCreateIngredient(Guid recipeId, string ingredientName, int quantity, Guid unitId) { if (quantity <= 0) @@ -123,6 +119,7 @@ // POST: /Recipe/Create/{categoryId} [HttpPost("Create/{categoryId}")] + [AutoValidateAntiforgeryToken] public async Task Create(Guid categoryId, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime, IFormFile? photo) { if (string.IsNullOrWhiteSpace(name)) @@ -153,6 +150,7 @@ return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); } + await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); var recipe = await _recipeRepository.CreateRecipeForCategoryAsync(categoryEntity, name, description, difficulty, servings, preparationTime, cookingTime); if (photo != null) { @@ -176,13 +174,13 @@ ViewBag.RecipeId = recipeId; ViewBag.IngredientId = ingredientId; - ViewBag.IngredientName = ingredient.Name; return View(); } // POST: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} [HttpPost("{recipeId}/RemoveIngredient/{ingredientId}")] + [ValidateAntiForgeryToken] public async Task RemoveIngredientConfirmed(Guid recipeId, Guid ingredientId) { await _recipeRepository.RemoveIngredientFromRecipeAsync(recipeId, ingredientId); @@ -219,6 +217,7 @@ // POST: /Recipe/{recipeId}/AddInstruction [HttpPost("{recipeId}/AddInstruction")] + [ValidateAntiForgeryToken] public async Task AddInstruction(Guid recipeId, string description, int number) { try @@ -239,23 +238,25 @@ [HttpGet("Favorites")] public async Task Favorites() { - var favoriteRecipes = await _favoritRepository.GetFavoriteRecipesAsync(); + var favoriteRecipes = await _favoriteRepository.GetFavoriteRecipesAsync(); return View(favoriteRecipes); } // POST: /Recipe/AddFavorite [HttpPost("AddFavorite")] + [ValidateAntiForgeryToken] public async Task AddFavorite(Guid recipeId) { - await _favoritRepository.AddFavoriteAsync(recipeId); + await _favoriteRepository.AddFavoriteAsync(recipeId); return RedirectToAction("Details", new { recipeId }); } // POST: /Recipe/RemoveFavorite [HttpPost("RemoveFavorite")] + [ValidateAntiForgeryToken] public async Task RemoveFavorite(Guid recipeId) { - await _favoritRepository.RemoveFavoriteAsync(recipeId); + await _favoriteRepository.RemoveFavoriteAsync(recipeId); return RedirectToAction("Details", new { recipeId }); } } diff --git a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs index 7f6a434..7747880 100644 --- a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs +++ b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs @@ -1,11 +1,11 @@ namespace Francesco.Recipes.World.Controllers { using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models; using Francesco.Recipes.World.Repositories.ShoppingList; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; - [Route("ShoppingList")] public class ShoppingListController : Controller { private readonly IShoppingListRepository _shoppingListRepository; @@ -18,7 +18,8 @@ } [HttpPost("CreateOrAddIngredients")] - public async Task CreateOrAddIngredients([FromBody] CreateOrAddIngredientsRequest request) + [ValidateAntiForgeryToken] + public async Task CreateOrAddIngredients([FromBody] CreateOrAddIngredientRequestModel request) { if (request == null || request.IngredientIds == null || !request.IngredientIds.Any()) { @@ -39,12 +40,5 @@ return Json(new { shoppingListId = shoppingList.Id }); } - - public class CreateOrAddIngredientsRequest - { - public Guid RecipeId { get; set; } - - public List IngredientIds { get; set; } = new (); - } } } diff --git a/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs b/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs index 1173425..d7ba63d 100644 --- a/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs +++ b/Francesco.Recipes.World/Models/BackendModels/RecipeShoppingList/RecipeShoppingList.cs @@ -10,7 +10,7 @@ public virtual ShoppingList ShoppingList { get; set; } = new ShoppingList(); - public virtual Recipe Recipe { get; set; } = new (); + public virtual Recipe Recipe { get; set; } = new Recipe(); public virtual ICollection SelectedIngredients { get; set; } = new List(); } diff --git a/Francesco.Recipes.World/Models/CreateOrAddIngredientRequestModel.cs b/Francesco.Recipes.World/Models/CreateOrAddIngredientRequestModel.cs new file mode 100644 index 0000000..8843bd8 --- /dev/null +++ b/Francesco.Recipes.World/Models/CreateOrAddIngredientRequestModel.cs @@ -0,0 +1,9 @@ +namespace Francesco.Recipes.World.Models +{ + public class CreateOrAddIngredientRequestModel + { + public Guid RecipeId { get; set; } + + public List IngredientIds { get; set; } = new (); + } +} diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index 6f98ff1..f5f164d 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -39,7 +39,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -64,6 +64,6 @@ app.MapDefaultControllerRoute(); app.MapControllerRoute( name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); + pattern: "{controller=Category}/{action=Index}/{id?}"); app.Run(); diff --git a/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs index 6e372f8..39e7228 100644 --- a/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs +++ b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs @@ -41,5 +41,12 @@ return category.Recipes; } + + public async Task> GetAllCategoriesWithRecipesAsync() + { + return await _context.Categories + .Include(c => c.Recipes) + .ToListAsync(); + } } } diff --git a/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs b/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs index 04491aa..f449de0 100644 --- a/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs +++ b/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs @@ -13,5 +13,7 @@ Task> GetAllCategoriesAsync(); Task> GetRecipesByCategoryAsync(Guid categoryId); + + Task> GetAllCategoriesWithRecipesAsync(); } } diff --git a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs index b4eddb5..eb8cae2 100644 --- a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs +++ b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs @@ -4,7 +4,7 @@ using Francesco.Recipes.World.Models.BackendModels.Recipe; using Microsoft.EntityFrameworkCore; - public class FavoritRepository : IFavoritRepository + public class FavoritRepository : IFavoriteRepository { private readonly FrancescosRecipesWorldDbContext _context; diff --git a/Francesco.Recipes.World/Repositories/Favorit/IFavoritRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/IFavoriteRepository.cs similarity index 89% rename from Francesco.Recipes.World/Repositories/Favorit/IFavoritRepository.cs rename to Francesco.Recipes.World/Repositories/Favorit/IFavoriteRepository.cs index 8f47e0a..bf1fd13 100644 --- a/Francesco.Recipes.World/Repositories/Favorit/IFavoritRepository.cs +++ b/Francesco.Recipes.World/Repositories/Favorit/IFavoriteRepository.cs @@ -2,7 +2,7 @@ { using Francesco.Recipes.World.Models.BackendModels.Recipe; - public interface IFavoritRepository + public interface IFavoriteRepository { Task> GetFavoriteRecipesAsync(); diff --git a/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs b/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs index 69a21ed..6f85a97 100644 --- a/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs +++ b/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs @@ -37,7 +37,6 @@ namespace Francesco.Recipes.World.Repositories.Ingredient existingRecipeIngredient.Ingredient = recipeIngredient.Ingredient; existingRecipeIngredient.Unit = recipeIngredient.Unit; - _context.RecipeIngredients.Update(existingRecipeIngredient); await _context.SaveChangesAsync(); } @@ -77,7 +76,6 @@ namespace Francesco.Recipes.World.Repositories.Ingredient existingIngredient.Name = ingredient.Name; - _context.Ingredients.Update(existingIngredient); await _context.SaveChangesAsync(); } diff --git a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs index 1d22238..963099b 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs @@ -1,7 +1,6 @@ namespace Francesco.Recipes.World.Repositories.Instruction { using Francesco.Recipes.World.Models.BackendModels.Instruction; - using Francesco.Recipes.World.Models.BackendModels.Recipe; public interface IInstructionRepository { @@ -11,6 +10,6 @@ Task> GetInstructionsByRecipeIdAsync(Guid recipeId); - Task RemoveInstructionFromRecipeAsync(Recipe recipe, Guid instructionId); + Task RemoveInstructionFromRecipeAsync(Guid recipeId, Guid instructionId); } } diff --git a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs index 09561c5..b487d57 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs @@ -2,7 +2,6 @@ { using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models.BackendModels.Instruction; - using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Repositories.Recipe; using Microsoft.EntityFrameworkCore; @@ -25,19 +24,19 @@ public async Task CreateInstructionToRecipeAsync(Guid recipeId, string description, int number) { - var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); - if (string.IsNullOrWhiteSpace(description)) + if (string.IsNullOrWhiteSpace(description)) { throw new ArgumentException("Description cannot be empty", nameof(description)); } - if (number <= 0) + if (number <= 0) { throw new ArgumentOutOfRangeException(nameof(number), "Number must be greater than 0."); } - var newInstruction = new Instruction + var newInstruction = new Instruction { Id = Guid.NewGuid(), Description = description, @@ -45,24 +44,28 @@ Recipe = recipe, }; - _context.Instructions.Add(newInstruction); - await _context.SaveChangesAsync(); + _context.Instructions.Add(newInstruction); + await _context.SaveChangesAsync(); - return newInstruction; + return newInstruction; } - public async Task RemoveInstructionFromRecipeAsync(Recipe recipe, Guid instructionId) + public async Task RemoveInstructionFromRecipeAsync(Guid recipeId, Guid instructionId) { - if (recipe == null) + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + + if (recipe.Instructions == null || !recipe.Instructions.Any()) { - throw new ArgumentNullException(nameof(recipe)); + await _context.Entry(recipe) + .Collection(r => r.Instructions) + .LoadAsync(); } - var instructionToRemove = recipe.Instructions.FirstOrDefault(i => i.Id == instructionId); + var instructionToRemove = recipe.Instructions?.FirstOrDefault(i => i.Id == instructionId); if (instructionToRemove != null) { - recipe.Instructions.Remove(instructionToRemove); + recipe.Instructions?.Remove(instructionToRemove); await _context.SaveChangesAsync(); } } diff --git a/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs index 879a1b2..9320d8c 100644 --- a/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs +++ b/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs @@ -2,7 +2,7 @@ { public interface IMediaFileRepository { - Task ReplaceInstructionImageAsync(Guid instructionId, Guid mediafileId, IFormFile? newPhoto); + Task ReplaceInstructionImageAsync(Guid instructionId, Guid mediaFileIdToReplace, string fileName, string mimeType, byte[] newMediaData); Task UploadInstructionImageAsync(Guid instructionId, IFormFile? photo); diff --git a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs index b4beb26..15f1fc6 100644 --- a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -2,6 +2,7 @@ { using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Models.BackendModels.MediaFile; + using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Repositories.Instruction; using Francesco.Recipes.World.Repositories.Recipe; @@ -18,13 +19,8 @@ _recipeRepository = recipeRepository; } - public async Task ReplaceInstructionImageAsync(Guid instructionId, Guid mediaFileIdToReplace, IFormFile? newPhoto) + public async Task ReplaceInstructionImageAsync(Guid instructionId, Guid mediaFileIdToReplace, string fileName, string mimeType, byte[] newMediaData) { - if (newPhoto is null) - { - throw new ArgumentNullException(nameof(newPhoto)); - } - var instruction = await _instructionRepository.GetInstructionAsync(instructionId); var mediaToReplace = instruction.MediaFiles.FirstOrDefault(m => m.Id == mediaFileIdToReplace); @@ -36,22 +32,17 @@ _context.MediaFiles.Remove(mediaToReplace); await _context.SaveChangesAsync(); - using (var memoryStream = new MemoryStream()) + var newMedia = new MediaFile { - await newPhoto.CopyToAsync(memoryStream); + Id = Guid.NewGuid(), + FileName = fileName, + MimeType = mimeType, + Data = newMediaData, + Instruction = instruction, + }; - var newMedia = new MediaFile - { - Id = Guid.NewGuid(), - FileName = newPhoto.FileName, - MimeType = newPhoto.ContentType, - Data = memoryStream.ToArray(), - Instruction = instruction, - }; - - _context.MediaFiles.Add(newMedia); - await _context.SaveChangesAsync(); - } + _context.MediaFiles.Add(newMedia); + await _context.SaveChangesAsync(); } public async Task UploadInstructionImageAsync(Guid instructionId, IFormFile? photo) @@ -89,11 +80,6 @@ var recipe = await _recipeRepository.GetRecipeAsync(recipeId); - if (recipe == null) - { - throw new InvalidOperationException("The specified recipe does not exist."); - } - var isImage = mediaFile.ContentType.StartsWith("image/"); var isVideo = mediaFile.ContentType.StartsWith("video/"); @@ -104,21 +90,11 @@ if (isImage) { - var existingImage = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType?.StartsWith("image/") == true); - if (existingImage != null) - { - _context.MediaFiles.Remove(existingImage); - await _context.SaveChangesAsync(); - } + await RemoveExistingMediaAsync(recipe, "image/"); } else if (isVideo) { - var existingVideo = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType?.StartsWith("video/") == true); - if (existingVideo != null) - { - _context.MediaFiles.Remove(existingVideo); - await _context.SaveChangesAsync(); - } + await RemoveExistingMediaAsync(recipe, "video/"); } using var memoryStream = new MemoryStream(); @@ -136,5 +112,15 @@ _context.MediaFiles.Add(newMedia); await _context.SaveChangesAsync(); } + + private async Task RemoveExistingMediaAsync(Recipe recipe, string mediaTypePrefix) + { + var existingMedia = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType?.StartsWith(mediaTypePrefix) == true); + if (existingMedia != null) + { + _context.MediaFiles.Remove(existingMedia); + await _context.SaveChangesAsync(); + } + } } } diff --git a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs index c5d46a0..03414e1 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -13,7 +13,7 @@ Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId); - Task> GetRecipesByNameOrIngredientAsync(string name, string ingredient); + Task> GetRecipesByNameAndIngredientAsync(string name, string ingredient); Task> GetRecipesByDifficultyAsync(Difficulty? difficulty); diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index ae33b43..1b7470a 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -37,6 +37,7 @@ .Include(r => r.RecipeIngredients) .ThenInclude(ri => ri.Unit) .Include(r => r.MediaFiles) + .Include(r => r.Instructions) .FirstOrDefaultAsync(r => r.Id == recipeId); } @@ -53,7 +54,7 @@ var ingredients = await _ingredientRepository.GetIngredientsByNameAsync(ingredientName); Ingredient ingredient; - if (ingredients == null || !ingredients.Any()) + if (!ingredients.Any()) { ingredient = new Ingredient { @@ -131,7 +132,6 @@ public async Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId) { - var recipe = await GetRecipeAsync(recipeId); var recipeIngredient = await _context.RecipeIngredients .FirstOrDefaultAsync(ri => ri.Recipe.Id == recipeId && ri.Ingredient.Id == ingredientId); @@ -144,7 +144,7 @@ await _context.SaveChangesAsync(); } - public async Task> GetRecipesByNameOrIngredientAsync(string name, string ingredient) + public async Task> GetRecipesByNameAndIngredientAsync(string name, string ingredient) { var query = _context.Recipes .Include(r => r.RecipeIngredients) diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs index 45793f0..c3e8590 100644 --- a/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs +++ b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs @@ -114,7 +114,6 @@ public async Task> GetIngredientsOfRecipeInListAsync(Guid shoppingListRecipeId) { return await _context.RecipeIngredientsShoppingLists - .AsNoTracking() .Include(i => i.RecipeIngredient) .ThenInclude(ri => ri.Ingredient) .Include(i => i.RecipeIngredient.Unit) @@ -168,7 +167,6 @@ public async Task GetRecipeByNameAndImageAsync(string recipeName, string imageFileName) { return await _context.Recipes - .AsNoTracking() .Where(r => r.Name == recipeName && r.MediaFiles.Any(mf => mf.FileName == imageFileName)) .Include(r => r.MediaFiles.Where(mf => mf.FileName == imageFileName)) .FirstOrDefaultAsync(); From 603dc4a2c60df7d72cb715cb587a8f50b7530fd2 Mon Sep 17 00:00:00 2001 From: franc Date: Thu, 10 Apr 2025 10:23:43 +0200 Subject: [PATCH 18/18] All thread resolved --- .../MediaFile/MediaFileController.cs | 2 +- .../ShoppingList/ShoppingListController.cs | 13 ++++++------- Francesco.Recipes.World/Program.cs | 1 - .../MediaFile/MediaFileRepository.cs | 2 +- .../Repositories/Recipe/RecipeRepository.cs | 7 +++++-- .../ShoppingList/IShoppingListRepository.cs | 3 ++- .../ShoppingList/ShoppingListRepository.cs | 18 +++++++----------- .../Views/Shared/_Layout.cshtml | 1 + 8 files changed, 23 insertions(+), 24 deletions(-) diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs index f021a8f..0d9ddf3 100644 --- a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -45,7 +45,7 @@ } [HttpPost("ReplaceInstructionImage")] - [ValidateAntiForgeryToken] + [AutoValidateAntiforgeryToken] public async Task ReplaceInstructionImage(Guid instructionId, Guid mediaFileIdToReplace, IFormFile? newPhoto) { if (newPhoto is null) diff --git a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs index 7747880..648cc7a 100644 --- a/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs +++ b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs @@ -4,7 +4,6 @@ using Francesco.Recipes.World.Models; using Francesco.Recipes.World.Repositories.ShoppingList; using Microsoft.AspNetCore.Mvc; - using Microsoft.EntityFrameworkCore; public class ShoppingListController : Controller { @@ -21,17 +20,17 @@ [ValidateAntiForgeryToken] public async Task CreateOrAddIngredients([FromBody] CreateOrAddIngredientRequestModel request) { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + if (request == null || request.IngredientIds == null || !request.IngredientIds.Any()) { return BadRequest("No ingredients provided."); } - await _shoppingListRepository.AddIngredientsToShoppingListAsync(request.RecipeId, request.IngredientIds); - - var shoppingList = await _context.ShoppingLists - .Include(sl => sl.RecipeShoppingList) - .ThenInclude(rsl => rsl.Recipe) - .FirstOrDefaultAsync(sl => sl.RecipeShoppingList.Any(rsl => rsl.Recipe.Id == request.RecipeId)); + var shoppingList = await _shoppingListRepository.AddIngredientsToShoppingListAsync(request.RecipeId, request.IngredientIds); if (shoppingList == null) { diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index f5f164d..40317b6 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -7,7 +7,6 @@ using Francesco.Recipes.World.Repositories.MediaFile; using Francesco.Recipes.World.Repositories.Recipe; using Francesco.Recipes.World.Repositories.ShoppingList; using Francesco.Recipes.World.Repositories.Unit; - using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); diff --git a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs index 15f1fc6..ef3babb 100644 --- a/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -92,7 +92,7 @@ { await RemoveExistingMediaAsync(recipe, "image/"); } - else if (isVideo) + else { await RemoveExistingMediaAsync(recipe, "video/"); } diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs index 1b7470a..f71303f 100644 --- a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -52,9 +52,11 @@ } var ingredients = await _ingredientRepository.GetIngredientsByNameAsync(ingredientName); + var exactMatch = ingredients + .FirstOrDefault(i => i.Name.Equals(ingredientName, StringComparison.OrdinalIgnoreCase)); Ingredient ingredient; - if (!ingredients.Any()) + if (exactMatch == null) { ingredient = new Ingredient { @@ -66,7 +68,7 @@ } else { - ingredient = ingredients.First(); + ingredient = exactMatch; } var existingEntry = await _context.RecipeIngredients @@ -83,6 +85,7 @@ Unit = unit, Quantity = quantity, }; + _context.Add(recipeIngredient); await _context.SaveChangesAsync(); } diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs index 5411e89..90c35c0 100644 --- a/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs +++ b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs @@ -2,10 +2,11 @@ { using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; public interface IShoppingListRepository { - Task AddIngredientsToShoppingListAsync(Guid shoppingListId, List ingredientIds); + Task AddIngredientsToShoppingListAsync(Guid recipeId, List ingredientIds); Task> GetIngredientsOfRecipeInListAsync(Guid shoppingListRecipeId); diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs index c3e8590..16d8934 100644 --- a/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs +++ b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs @@ -16,7 +16,7 @@ _context = context; } - public async Task AddIngredientsToShoppingListAsync(Guid recipeId, List ingredientIds) + public async Task AddIngredientsToShoppingListAsync(Guid recipeId, List ingredientIds) { if (ingredientIds == null || !ingredientIds.Any()) { @@ -29,13 +29,12 @@ if (recipe == null) { - throw new Exception("Rezept nicht gefunden."); + throw new Exception("Recipe not found."); } var shoppingList = await _context.ShoppingLists .Include(sl => sl.RecipeShoppingList) .ThenInclude(rsl => rsl.SelectedIngredients) - .ThenInclude(si => si.RecipeIngredient) .Include(sl => sl.RecipeShoppingList) .ThenInclude(rsl => rsl.Recipe) .FirstOrDefaultAsync(sl => sl.RecipeShoppingList.Any(rsl => rsl.Recipe.Id == recipeId)); @@ -46,7 +45,7 @@ if (!recipeIngredients.Any()) { - throw new Exception("Keine gültigen Zutaten gefunden."); + throw new Exception("No valid ingredients found."); } if (shoppingList == null) @@ -55,7 +54,6 @@ { Id = Guid.NewGuid(), CreatedAt = DateTime.UtcNow, - ModifiedAt = null, RecipeShoppingList = new List(), }; @@ -93,10 +91,7 @@ foreach (var ri in recipeIngredients) { - var alreadyExists = existingRecipeList.SelectedIngredients - .Any(si => si.RecipeIngredient.Id == ri.Id); - - if (!alreadyExists) + if (!existingRecipeList.SelectedIngredients.Any(si => si.RecipeIngredient.Id == ri.Id)) { existingRecipeList.SelectedIngredients.Add(new RecipeIngredientShoppingList { @@ -109,6 +104,9 @@ shoppingList.ModifiedAt = DateTime.UtcNow; } + + await _context.SaveChangesAsync(); + return shoppingList; } public async Task> GetIngredientsOfRecipeInListAsync(Guid shoppingListRecipeId) @@ -153,8 +151,6 @@ public async Task DeleteShoppingListAsync(Guid shoppingListId) { var list = await _context.ShoppingLists - .Include(sl => sl.RecipeShoppingList) - .ThenInclude(r => r.SelectedIngredients) .FirstOrDefaultAsync(sl => sl.Id == shoppingListId); if (list != null) diff --git a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml index 1f862ba..9d445a9 100644 --- a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml @@ -44,6 +44,7 @@ + @await RenderSectionAsync("Scripts", required: false)