diff --git a/Francesco.Recipes.World/Controller/Category/CategoryController.cs b/Francesco.Recipes.World/Controller/Category/CategoryController.cs new file mode 100644 index 0000000..f077af7 --- /dev/null +++ b/Francesco.Recipes.World/Controller/Category/CategoryController.cs @@ -0,0 +1,39 @@ +namespace Francesco.Recipes.World.Controller.Category +{ + using Francesco.Recipes.World.Repositories.Category; + using Microsoft.AspNetCore.Mvc; + + 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..0d9ddf3 --- /dev/null +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -0,0 +1,74 @@ +namespace Francesco.Recipes.World.Controller.MediaFile +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Repositories.MediaFile; + using Microsoft.AspNetCore.Mvc; + + [Route("Category/{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")] + [AutoValidateAntiforgeryToken] + 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(); + } + + [HttpPost("ReplaceInstructionImage")] + [AutoValidateAntiforgeryToken] + 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 new file mode 100644 index 0000000..b86ef61 --- /dev/null +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -0,0 +1,263 @@ +namespace Francesco.Recipes.World.Controller.Recipe +{ + 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; + + 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 IFavoriteRepository _favoriteRepository; + + public IReadOnlyCollection Recipes { get; set; } + + public RecipeController( + IRecipeRepository recipeRepository, + IUnitRepository unitRepository, + ICategoryRepository categoryRepository, + IIngredientRepository ingredientRepository, + IMediaFileRepository mediaFileRepository, + IInstructionRepository instructionRepository, + IFavoriteRepository favoriteRepository) + { + _recipeRepository = recipeRepository; + _unitRepository = unitRepository; + _categoryRepository = categoryRepository; + _ingredientRepository = ingredientRepository; + Recipes = new List(); + _mediaFileRepository = mediaFileRepository; + _instructionRepository = instructionRepository; + _favoriteRepository = favoriteRepository; + } + + // GET: /Recipe/{recipeId}/AddOrCreateIngredient + [HttpGet("{recipeId}/AddOrCreateIngredient")] + public async Task AddOrCreateIngredient(Guid recipeId) + { + 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.GetAllCategoriesWithRecipesAsync(); + + var viewModel = categories.Select(c => new CategoryRecipesViewModel + { + 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) + { + 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: /Recipe/Create/{categoryId} + [HttpGet("Create/{categoryId}")] + 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: /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)) + { + 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); + 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: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} + [HttpGet("{recipeId}/RemoveIngredient/{ingredientId}")] + public async Task RemoveIngredient(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; + + 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); + TempData["SuccessMessage"] = "Ingredient removed successfully."; + return RedirectToAction("Details", new { id = recipeId }); + } + + // GET: /Recipe/FilterByDifficulty + [HttpGet("FilterByDifficulty")] + public async Task FilterByDifficulty(Difficulty? selectedDifficulty) + { + 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")] + [ValidateAntiForgeryToken] + 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 _favoriteRepository.GetFavoriteRecipesAsync(); + return View(favoriteRecipes); + } + + // POST: /Recipe/AddFavorite + [HttpPost("AddFavorite")] + [ValidateAntiForgeryToken] + public async Task AddFavorite(Guid recipeId) + { + await _favoriteRepository.AddFavoriteAsync(recipeId); + return RedirectToAction("Details", new { recipeId }); + } + + // POST: /Recipe/RemoveFavorite + [HttpPost("RemoveFavorite")] + [ValidateAntiForgeryToken] + public async Task RemoveFavorite(Guid 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 new file mode 100644 index 0000000..648cc7a --- /dev/null +++ b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs @@ -0,0 +1,43 @@ +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; + + public class ShoppingListController : Controller + { + private readonly IShoppingListRepository _shoppingListRepository; + private readonly FrancescosRecipesWorldDbContext _context; + + public ShoppingListController(IShoppingListRepository shoppingListRepository, FrancescosRecipesWorldDbContext context) + { + _shoppingListRepository = shoppingListRepository; + _context = context; + } + + [HttpPost("CreateOrAddIngredients")] + [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."); + } + + var shoppingList = await _shoppingListRepository.AddIngredientsToShoppingListAsync(request.RecipeId, request.IngredientIds); + + if (shoppingList == null) + { + return BadRequest("Error creating shopping list."); + } + + return Json(new { shoppingListId = shoppingList.Id }); + } + } +} 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 + { + } +} diff --git a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs index 67117c4..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; @@ -34,7 +35,9 @@ public DbSet Favorits => Set(); - public DbSet IngredientsShoppingLists => Set(); + public DbSet RecipeIngredientsShoppingLists => Set(); + + public DbSet RecipeShoppingLists => Set(); public DbSet ShoppingLists => Set(); diff --git a/Francesco.Recipes.World/Francesco.Recipes.World.csproj b/Francesco.Recipes.World/Francesco.Recipes.World.csproj index 728f575..7d430a2 100644 --- a/Francesco.Recipes.World/Francesco.Recipes.World.csproj +++ b/Francesco.Recipes.World/Francesco.Recipes.World.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -29,6 +29,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -41,12 +42,16 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + + + + 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/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 130fbf5..c27540a 100644 --- a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs +++ b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs @@ -1,9 +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 @@ -117,25 +118,33 @@ 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("IsChecked") + .HasColumnType("bit"); + + b.Property("RecipeIngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("RecipeShoppingListId") .HasColumnType("uniqueidentifier"); b.HasKey("Id"); b.HasIndex("IngredientId"); - b.HasIndex("ShoppinglistId"); + b.HasIndex("RecipeIngredientId"); - b.ToTable("IngredientsShoppingLists"); + b.HasIndex("RecipeShoppingListId"); + + b.ToTable("RecipeIngredientsShoppingLists"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => @@ -148,9 +157,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"); @@ -198,7 +206,7 @@ namespace Francesco.Recipes.World.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("CategoryId") + b.Property("CategoryId") .HasColumnType("uniqueidentifier"); b.Property("CookingTime") @@ -270,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") @@ -374,23 +403,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.RecipeShoppingList.RecipeShoppingList", "RecipeShoppingList") + .WithMany("SelectedIngredients") + .HasForeignKey("RecipeShoppingListId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Ingredient"); + b.Navigation("RecipeIngredient"); - b.Navigation("Shoppinglist"); + b.Navigation("RecipeShoppingList"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => @@ -423,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") @@ -433,6 +468,8 @@ namespace Francesco.Recipes.World.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Category"); + b.Navigation("Favorit"); }); @@ -463,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"); @@ -494,9 +550,14 @@ 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("IngredientsShoppingLists"); + b.Navigation("RecipeIngredientShoppingLists"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => 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/Difficulty.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs index 4456670..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 { - VeryEasy = 1, + [Display(Name = "Sehr einfach")] + VeryEasy = 0, - Easy = 2, + [Display(Name = "Einfach")] + Easy = 1, - Medium = 3, + [Display(Name = "Mittel")] + Medium = 2, - Hard = 4, + [Display(Name = "Schwer")] + Hard = 3, - Expert = 5, + [Display(Name = "Experte")] + Expert = 4, } } 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..fa691b8 --- /dev/null +++ b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredientShoppingList/RecipeIngredientShoppingList.cs @@ -0,0 +1,16 @@ +namespace Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList +{ + using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; + using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; + + public class RecipeIngredientShoppingList + { + public Guid Id { get; set; } + + 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..d7ba63d --- /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 Recipe(); + + 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 71fc7e7..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 IngredientsShoppingLists { get; set; } = new List(); + public virtual ICollection RecipeShoppingList { 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 6d773ec..40317b6 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -1,11 +1,12 @@ using Francesco.Recipes.World.Data; - -using Francesco.Recipes.World.Repositories; - -using FrancescoRecipesWorld.Repositories; - -using Microsoft.AspNetCore.Identity; - +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.ShoppingList; +using Francesco.Recipes.World.Repositories.Unit; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -20,9 +21,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(); @@ -34,6 +32,14 @@ builder.Services.AddScoped(); 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,20 +51,18 @@ if (!app.Environment.IsDevelopment()) app.UseHsts(); } +Console.WriteLine("Standard Numeric Format Specifiers"); + app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); -app.UseAuthentication(); - -app.UseAuthorization(); - 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 new file mode 100644 index 0000000..39e7228 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs @@ -0,0 +1,52 @@ +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; + } + + 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 new file mode 100644 index 0000000..f449de0 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs @@ -0,0 +1,19 @@ +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); + + Task> GetAllCategoriesWithRecipesAsync(); + } +} diff --git a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs new file mode 100644 index 0000000..eb8cae2 --- /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 : IFavoriteRepository + { + 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/IFavoriteRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/IFavoriteRepository.cs new file mode 100644 index 0000000..bf1fd13 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Favorit/IFavoriteRepository.cs @@ -0,0 +1,15 @@ +namespace Francesco.Recipes.World.Repositories.Favorit +{ + using Francesco.Recipes.World.Models.BackendModels.Recipe; + + public interface IFavoriteRepository + { + Task> GetFavoriteRecipesAsync(); + + Task IsFavoriteAsync(Guid recipeId); + + Task AddFavoriteAsync(Guid recipeId); + + Task RemoveFavoriteAsync(Guid recipeId); + } +} diff --git a/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs b/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs new file mode 100644 index 0000000..702f174 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Ingredient/IIngredientRepository.cs @@ -0,0 +1,16 @@ +namespace Francesco.Recipes.World.Repositories.Ingredient +{ + using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; + + public interface IIngredientRepository + { + Task UpdateIngredientAsync(Ingredient ingredient); + + Task> GetIngredientsByRecipeIdAsync(Guid recipeId); + + Task> GetIngredientsByNameAsync(string name); + + 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..6f85a97 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Ingredient/IngredientRepository.cs @@ -0,0 +1,88 @@ + +namespace Francesco.Recipes.World.Repositories.Ingredient +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.Ingredient; + 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 UpdateRecipeIngredientAsync(RecipeIngredient recipeIngredient) + { + if (recipeIngredient == null) + { + throw new ArgumentNullException(nameof(recipeIngredient)); + } + + var existingRecipeIngredient = await _context.RecipeIngredients + .Include(ri => ri.Ingredient) + .Include(ri => ri.Unit) + .FirstOrDefaultAsync(ri => ri.Id == recipeIngredient.Id); + + if (existingRecipeIngredient == null) + { + throw new InvalidOperationException($"RecipeIngredient with ID {recipeIngredient.Id} not found."); + } + + existingRecipeIngredient.Quantity = recipeIngredient.Quantity; + existingRecipeIngredient.Ingredient = recipeIngredient.Ingredient; + existingRecipeIngredient.Unit = recipeIngredient.Unit; + + 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; + + 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..963099b --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs @@ -0,0 +1,15 @@ +namespace Francesco.Recipes.World.Repositories.Instruction +{ + using Francesco.Recipes.World.Models.BackendModels.Instruction; + + public interface IInstructionRepository + { + Task GetInstructionAsync(Guid instructionId); + + Task CreateInstructionToRecipeAsync(Guid recipeId, string description, int number); + + Task> GetInstructionsByRecipeIdAsync(Guid recipeId); + + Task RemoveInstructionFromRecipeAsync(Guid recipeId, 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..b487d57 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs @@ -0,0 +1,81 @@ +namespace Francesco.Recipes.World.Repositories.Instruction +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.Instruction; + 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, IRecipeRepository recipeRepository) + { + _recipeRepository = recipeRepository; + _context = context; + } + + public async Task GetInstructionAsync(Guid instructionId) + { + 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(Guid recipeId, Guid instructionId) + { + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + + if (recipe.Instructions == null || !recipe.Instructions.Any()) + { + await _context.Entry(recipe) + .Collection(r => r.Instructions) + .LoadAsync(); + } + + 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(); + } + } +} diff --git a/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs b/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs new file mode 100644 index 0000000..9320d8c --- /dev/null +++ b/Francesco.Recipes.World/Repositories/MediaFile/IMediaFileRepository.cs @@ -0,0 +1,11 @@ +namespace Francesco.Recipes.World.Repositories.MediaFile +{ + public interface IMediaFileRepository + { + Task ReplaceInstructionImageAsync(Guid instructionId, Guid mediaFileIdToReplace, string fileName, string mimeType, byte[] newMediaData); + + Task UploadInstructionImageAsync(Guid instructionId, 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 new file mode 100644 index 0000000..ef3babb --- /dev/null +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -0,0 +1,126 @@ +namespace Francesco.Recipes.World.Repositories.MediaFile +{ + 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; + + 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, Guid mediaFileIdToReplace, string fileName, string mimeType, byte[] newMediaData) + { + var instruction = await _instructionRepository.GetInstructionAsync(instructionId); + + var mediaToReplace = instruction.MediaFiles.FirstOrDefault(m => m.Id == mediaFileIdToReplace); + if (mediaToReplace == null) + { + throw new InvalidOperationException("The specified media file does not exist."); + } + + _context.MediaFiles.Remove(mediaToReplace); + await _context.SaveChangesAsync(); + + var newMedia = new MediaFile + { + Id = Guid.NewGuid(), + FileName = fileName, + MimeType = mimeType, + Data = newMediaData, + Instruction = instruction, + }; + + _context.MediaFiles.Add(newMedia); + await _context.SaveChangesAsync(); + } + + 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 UploadRecipeMediaAsync(Guid recipeId, IFormFile mediaFile) + { + if (mediaFile == null) + { + throw new ArgumentNullException(nameof(mediaFile)); + } + + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + + 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) + { + await RemoveExistingMediaAsync(recipe, "image/"); + } + else + { + await RemoveExistingMediaAsync(recipe, "video/"); + } + + 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(); + } + + 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 new file mode 100644 index 0000000..03414e1 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -0,0 +1,22 @@ +namespace Francesco.Recipes.World.Repositories.Recipe +{ + using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + + 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); + + Task> GetRecipesByNameAndIngredientAsync(string name, string ingredient); + + Task> GetRecipesByDifficultyAsync(Difficulty? difficulty); + + 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..f71303f --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -0,0 +1,195 @@ +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.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 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) + .Include(r => r.Instructions) + .FirstOrDefaultAsync(r => r.Id == recipeId); + } + + 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); + var exactMatch = ingredients + .FirstOrDefault(i => i.Name.Equals(ingredientName, StringComparison.OrdinalIgnoreCase)); + Ingredient ingredient; + + if (exactMatch == null) + { + ingredient = new Ingredient + { + Id = Guid.NewGuid(), + Name = ingredientName, + }; + _context.Ingredients.Add(ingredient); + await _context.SaveChangesAsync(); + } + else + { + ingredient = exactMatch; + } + + 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 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> GetRecipesByNameAndIngredientAsync(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) + { + if (!difficulty.HasValue) + { + return await _context.Recipes + .Include(r => r.Category) + .Include(r => r.RecipeIngredients) + .ThenInclude(ri => ri.Ingredient) + .ToListAsync(); + } + + return await _context.Recipes + .Where(r => r.Difficulty == difficulty.Value) + .Include(r => r.Category) + .Include(r => r.RecipeIngredients) + .ThenInclude(ri => ri.Ingredient) + .ToListAsync(); + } + } +} diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs new file mode 100644 index 0000000..90c35c0 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs @@ -0,0 +1,21 @@ +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 AddIngredientsToShoppingListAsync(Guid recipeId, List ingredientIds); + + Task> GetIngredientsOfRecipeInListAsync(Guid shoppingListRecipeId); + + Task UpdateIngredientCheckedAsync(Guid shoppingListRecipeId, Guid recipeIngredientId, bool isChecked); + + 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 new file mode 100644 index 0000000..16d8934 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs @@ -0,0 +1,171 @@ +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.RecipeShoppingList; + 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 AddIngredientsToShoppingListAsync(Guid recipeId, List ingredientIds) + { + if (ingredientIds == null || !ingredientIds.Any()) + { + throw new ArgumentNullException(nameof(ingredientIds)); + } + + var recipe = await _context.Recipes + .Include(r => r.RecipeIngredients) + .FirstOrDefaultAsync(r => r.Id == recipeId); + + if (recipe == null) + { + throw new Exception("Recipe not found."); + } + + var shoppingList = await _context.ShoppingLists + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.SelectedIngredients) + .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("No valid ingredients found."); + } + + if (shoppingList == null) + { + shoppingList = new ShoppingList + { + Id = Guid.NewGuid(), + CreatedAt = DateTime.UtcNow, + 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) + { + if (!existingRecipeList.SelectedIngredients.Any(si => si.RecipeIngredient.Id == ri.Id)) + { + existingRecipeList.SelectedIngredients.Add(new RecipeIngredientShoppingList + { + Id = Guid.NewGuid(), + RecipeIngredient = ri, + IsChecked = false, + }); + } + } + + shoppingList.ModifiedAt = DateTime.UtcNow; + } + + await _context.SaveChangesAsync(); + return shoppingList; + } + + public async Task> GetIngredientsOfRecipeInListAsync(Guid shoppingListRecipeId) + { + return await _context.RecipeIngredientsShoppingLists + .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."); + } + + item.IsChecked = isChecked; + await _context.SaveChangesAsync(); + } + + public async Task RemoveRecipeIfEmptyAsync(Guid shoppingListRecipeId) + { + var recipeEntry = await _context.RecipeShoppingLists + .Include(r => r.SelectedIngredients) + .FirstOrDefaultAsync(r => r.Id == shoppingListRecipeId); + + if (recipeEntry != null && !recipeEntry.SelectedIngredients.Any()) + { + _context.RecipeShoppingLists.Remove(recipeEntry); + await _context.SaveChangesAsync(); + } + } + + public async Task DeleteShoppingListAsync(Guid shoppingListId) + { + var list = await _context.ShoppingLists + .FirstOrDefaultAsync(sl => sl.Id == shoppingListId); + + if (list != null) + { + _context.ShoppingLists.Remove(list); + await _context.SaveChangesAsync(); + } + } + + 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(); + } + } +} 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/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/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 new file mode 100644 index 0000000..1232469 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/AddOrCreateIngredient.cshtml @@ -0,0 +1,27 @@ +@{ + 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 new file mode 100644 index 0000000..e8c3769 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/Create.cshtml @@ -0,0 +1,58 @@ +@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/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 new file mode 100644 index 0000000..b0e499e --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/FilterByDifficulty.cshtml @@ -0,0 +1,72 @@ +@using Francesco.Recipes.World.Models.BackendModels.Recipe +@model Francesco.Recipes.World.Views.Recipe.FilterByDifficultyViewModel + +

Rezepte nach Schwierigkeitsgrad

+ + + +
+
+
+
+ + +
+
+ +
+
+ +
+ @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 { + +} diff --git a/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs b/Francesco.Recipes.World/Views/Recipe/FilterByDifficultyViewModel.cs new file mode 100644 index 0000000..b0543f3 --- /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 IReadOnlyCollection 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") +} 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/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) 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
  • + } +
+