diff --git a/.editorconfig b/.editorconfig index 367a88a..6e06f07 100644 --- a/.editorconfig +++ b/.editorconfig @@ -126,7 +126,7 @@ dotnet_diagnostic.SA1601.severity = none dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = none # SA1516: Elements should be separated by blank line -dotnet_diagnostic.SA1516.severity = none +dotnet_diagnostic.SA1516.severity = error # SA1649: File name should match first type name dotnet_diagnostic.SA1649.severity = none @@ -142,3 +142,11 @@ dotnet_diagnostic.SA1309.severity = none # SA1309: Make sure class members are allowed to call without "this" prefix dotnet_diagnostic.SA1101.severity = none +# SA1118: parameter spans multiple lines +dotnet_diagnostic.SA1118.severity = none +# SA1011: closing square bracket spacing +dotnet_diagnostic.SA1011.severity = none +# SA1517: no blank lines at start of file +dotnet_diagnostic.SA1517.severity = none +# SA1200: using directive should appear within a namespace +dotnet_diagnostic.SA1200.severity = none diff --git a/.gitignore b/.gitignore index 61101b4..ed90c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -396,4 +396,5 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml -src/Recruitment.Tool.xml +src/Francesco.Recipes.World.xml + diff --git a/Francesco.Recipes.World.sln b/Francesco.Recipes.World.sln index 793bcdf..998bb15 100644 --- a/Francesco.Recipes.World.sln +++ b/Francesco.Recipes.World.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.11.35222.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Francesco.Recipes.World", "Francesco.Recipes.World\Francesco.Recipes.World.csproj", "{4D1BBCF4-8E06-4584-A383-B14BEC558408}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrancescosRecipeWorld Mock", "FrancescosRecipeWorld Mock\FrancescosRecipeWorld Mock.csproj", "{04FD3555-3497-4F6B-B2C3-1B22EEFA676B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {4D1BBCF4-8E06-4584-A383-B14BEC558408}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D1BBCF4-8E06-4584-A383-B14BEC558408}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D1BBCF4-8E06-4584-A383-B14BEC558408}.Release|Any CPU.Build.0 = Release|Any CPU + {04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04FD3555-3497-4F6B-B2C3-1B22EEFA676B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Francesco.Recipes.World/Constants/ContentType.cs b/Francesco.Recipes.World/Constants/ContentType.cs new file mode 100644 index 0000000..8f7d090 --- /dev/null +++ b/Francesco.Recipes.World/Constants/ContentType.cs @@ -0,0 +1,7 @@ +namespace Francesco.Recipes.World.Constants +{ + public class ContentType + { + public const string Image = "image/"; + } +} diff --git a/Francesco.Recipes.World/Constants/SortOrders.cs b/Francesco.Recipes.World/Constants/SortOrders.cs new file mode 100644 index 0000000..479261c --- /dev/null +++ b/Francesco.Recipes.World/Constants/SortOrders.cs @@ -0,0 +1,8 @@ +namespace Francesco.Recipes.World.Constants +{ + public class SortOrders + { + public const string Newest = "newest"; + public const string Oldest = "oldest"; + } +} 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/Favorite/FavoriteController.cs b/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs new file mode 100644 index 0000000..9cd9c7a --- /dev/null +++ b/Francesco.Recipes.World/Controller/Favorite/FavoriteController.cs @@ -0,0 +1,35 @@ +namespace Francesco.Recipes.World.Controller.Favorite +{ + using Francesco.Recipes.World.Constants; + using Francesco.Recipes.World.Models; + using Francesco.Recipes.World.Repositories.Favorit; + using Microsoft.AspNetCore.Mvc; + + [Route("Favorite")] + public class FavoriteController : Controller + { + private readonly IFavoriteRepository _favoriteRepository; + + public FavoriteController(IFavoriteRepository favoriteRepository) + { + _favoriteRepository = favoriteRepository; + } + + public async Task Index(string sortOrder = SortOrders.Newest) + { + var favoriteRecipes = await _favoriteRepository.GetFavoriteRecipesAsync(); + + var sortedRecipes = sortOrder == SortOrders.Oldest + ? favoriteRecipes.OrderBy(r => r.Favorite.CreatedAt) + : favoriteRecipes.OrderByDescending(r => r.Favorite.CreatedAt); + + var viewModel = new FavoriteViewModel + { + FavoriteRecipes = sortedRecipes, + SortOrder = sortOrder, + }; + + return View(viewModel); + } + } +} diff --git a/Francesco.Recipes.World/Controller/HomeController/HomeController.cs b/Francesco.Recipes.World/Controller/HomeController/HomeController.cs new file mode 100644 index 0000000..88df8bc --- /dev/null +++ b/Francesco.Recipes.World/Controller/HomeController/HomeController.cs @@ -0,0 +1,40 @@ +namespace Francesco.Recipes.World.Controller.HomeController +{ + using Francesco.Recipes.World.Repositories.Category; + using Francesco.Recipes.World.Repositories.Recipe; + using Francesco.Recipes.World.Views.Category; + using Microsoft.AspNetCore.Mvc; + + public class HomeController : Controller + { + private readonly IRecipeRepository _recipeRepository; + private readonly ICategoryRepository _categoryRepository; + + public HomeController(ICategoryRepository categoryRepository, IRecipeRepository recipeRepository) + { + _recipeRepository = recipeRepository; + _categoryRepository = categoryRepository; + } + + [HttpGet] + public async Task Index() + { + var categories = await _categoryRepository.GetAllCategoriesWithRecipesAsync(); + + var viewModel = categories.Select(category => new CategoryRecipesViewModel + { + Category = category, + Recipes = category.Recipes, + }); + + return View("Index", viewModel); + } + + [HttpGet("/Home/Search")] + public async Task Search(string term) + { + var recipes = await _recipeRepository.SearchInRecipesAndIngredients(term); + return PartialView("_SearchResultsPartial", 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..a770783 --- /dev/null +++ b/Francesco.Recipes.World/Controller/Instruction/InstructionController.cs @@ -0,0 +1,69 @@ +namespace Francesco.Recipes.World.Controller.Instruction +{ + using Francesco.Recipes.World.Models; + using Francesco.Recipes.World.Repositories.Instruction; + using Francesco.Recipes.World.Services.Instruction; + using Microsoft.AspNetCore.Mvc; + + public class InstructionController : Controller + { + private readonly IInstructionService _instructionService; + private readonly IInstructionRepository _instructionRepository; + + public InstructionController(IInstructionService instructionService, IInstructionRepository instructionRepository) + { + _instructionService = instructionService; + _instructionRepository = instructionRepository; + } + + [HttpPost("Recipe/{recipeId}/Instruction/{instructionId}/move-up")] + [ValidateAntiForgeryToken] + public async Task MoveUp(Guid recipeId, Guid instructionId) + { + try + { + await _instructionService.MoveInstructionUpAsync(recipeId, instructionId); + return Ok(new { Message = "Instruction moved up successfully." }); + } + catch (Exception ex) + { + return BadRequest(new { Error = ex.Message }); + } + } + + [HttpPost("Recipe/{recipeId}/Instruction/{instructionId}/move-down")] + [ValidateAntiForgeryToken] + public async Task MoveDown(Guid recipeId, Guid instructionId) + { + try + { + await _instructionService.MoveInstructionDownAsync(recipeId, instructionId); + return Ok(new { Message = "Instruction moved down successfully." }); + } + catch (Exception ex) + { + return BadRequest(new { Error = ex.Message }); + } + } + + [HttpGet("Recipe/{recipeId}/Instructions")] + public async Task GetInstructions(Guid recipeId) + { + try + { + var sortedInstructions = await _instructionService.GetSortedInstructionsAsync(recipeId); + var viewModel = new InstructionViewModel + { + RecipeId = recipeId, + Instructions = sortedInstructions, + }; + + return View("~/Views/Shared/_GetInstructions.cshtml", viewModel); + } + catch (Exception ex) + { + return BadRequest(new { Error = ex.Message }); + } + } + } +} diff --git a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs new file mode 100644 index 0000000..d0bfe25 --- /dev/null +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -0,0 +1,97 @@ +namespace Francesco.Recipes.World.Controller.MediaFile +{ + using Francesco.Recipes.World.Repositories.MediaFile; + using Microsoft.AspNetCore.Mvc; + + [Route("Category/{categoryId}/Recipe")] + public class MediaFileController : Controller + { + private readonly IMediaFileRepository _mediaFileRepository; + + public MediaFileController(IMediaFileRepository mediaFileRepository) + { + _mediaFileRepository = mediaFileRepository; + } + + // POST: /UploadImage + [HttpPost("UploadImage")] + [ValidateAntiForgeryToken] + 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")] + [ValidateAntiForgeryToken] + public async Task ReplaceInstructionImage(Guid instructionId, Guid mediaFileIdToReplace, IFormFile? newPhoto) + { + if (newPhoto is null) + { + return BadRequest("Photo is required."); + } + + using (var memoryStream = new MemoryStream()) + { + await newPhoto.CopyToAsync(memoryStream); + + var newMediaData = memoryStream.ToArray(); + + try + { + await _mediaFileRepository.ReplaceInstructionImageAsync(instructionId, mediaFileIdToReplace, newPhoto.FileName, newPhoto.ContentType, newMediaData); + return Ok("Image replaced successfully."); + } + catch (Exception ex) + { + return StatusCode(500, $"Internal server error: {ex.Message}"); + } + } + } + + [HttpPost("Recipe/{recipeId}/Instruction/{instructionId}/UploadImage")] + [ValidateAntiForgeryToken] + public async Task UploadInstructionImage(Guid recipeId, Guid instructionId, IFormFile? photo) + { + if (recipeId == Guid.Empty) + { + return BadRequest("Recipe ID is required."); + } + + if (photo == null) + { + return BadRequest("Photo is required."); + } + + try + { + await _mediaFileRepository.UploadInstructionImageAsync(instructionId, photo); + + return RedirectToAction("GetInstructions", "Instruction", new { recipeId }); + } + 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..46177ee --- /dev/null +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -0,0 +1,519 @@ +namespace Francesco.Recipes.World.Controller.Recipe +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models; + using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.Instruction; + 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; + private readonly FrancescosRecipesWorldDbContext _context; + + public IReadOnlyCollection Recipes { get; set; } + + public RecipeController( + IRecipeRepository recipeRepository, + IUnitRepository unitRepository, + ICategoryRepository categoryRepository, + IIngredientRepository ingredientRepository, + IMediaFileRepository mediaFileRepository, + IInstructionRepository instructionRepository, + IFavoriteRepository favoriteRepository, + FrancescosRecipesWorldDbContext context) + { + _recipeRepository = recipeRepository; + _unitRepository = unitRepository; + _categoryRepository = categoryRepository; + _ingredientRepository = ingredientRepository; + Recipes = new List(); + _mediaFileRepository = mediaFileRepository; + _instructionRepository = instructionRepository; + _favoriteRepository = favoriteRepository; + _context = context; + } + + // GET: /Recipe/{recipeId}/AddOrCreateIngredient + [HttpGet("{recipeId}/AddOrCreateIngredient")] + public async Task AddOrCreateIngredient(Guid recipeId) + { + if (recipeId == Guid.Empty) + { + return BadRequest("Recipe ID cannot be empty."); + } + + var units = await _unitRepository.GetAllUnitsAsync(); + var recipeIngredients = await _ingredientRepository.GetIngredientsByRecipeIdAsync(recipeId); + var ingredients = recipeIngredients.Select(ri => ri.Ingredient).ToList(); + + var viewModel = new IngredientViewModel + { + RecipeId = recipeId, + Ingredients = ingredients, + Units = units.ToList(), + }; + + return View(viewModel); + } + + // 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 (recipeId == Guid.Empty) + { + return BadRequest("Recipe ID cannot be empty."); + } + + 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.CreateRecipeIngredientAsync(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."); + } + + var units = await _unitRepository.GetAllUnitsAsync(); + + var viewModel = new CreateRecipeViewModel + { + CategoryId = categoryId, + CategoryName = category.Name, + IngredientViewModel = new IngredientViewModel + { + RecipeId = Guid.Empty, + Ingredients = new List(), + Units = units.ToList(), + }, + InstructionViewModel = new InstructionViewModel + { + RecipeId = Guid.Empty, + Instructions = new List(), + }, + }; + return View(viewModel); + } + + // POST: /Recipe/Create/{categoryId} + [HttpPost("Create/{categoryId}")] + [ValidateAntiForgeryToken] + public async Task Create(Guid categoryId, CreateRecipeViewModel model) + { + if (model == null) + { + throw new ArgumentNullException(nameof(model), "CreateRecipeViewModel cannot be null."); + } + + if (string.IsNullOrWhiteSpace(model.Name)) + { + ModelState.AddModelError(nameof(model.Name), "Name darf nicht leer sein."); + } + + if (model.Servings <= 0) + { + ModelState.AddModelError(nameof(model.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."); + } + + var units = await _unitRepository.GetAllUnitsAsync(); + + if (model.IngredientViewModel == null) + { + model.IngredientViewModel = new IngredientViewModel + { + RecipeId = Guid.Empty, + Ingredients = new List(), + Units = units.ToList(), + }; + } + else + { + model.IngredientViewModel.Units = units.ToList(); + } + + model.CategoryName = category.Name; + return View(model); + } + + using var transaction = await _context.Database.BeginTransactionAsync(); + try + { + var categoryEntity = await _categoryRepository.GetCategoryByIdAsync(categoryId); + if (categoryEntity == null) + { + return NotFound($"Kategorie mit ID {categoryId} wurde nicht gefunden."); + } + + model.PreparationTime = new TimeSpan(model.PrepHours, model.PrepMinutes, 0); + model.CookingTime = new TimeSpan(model.CookHours, model.CookMinutes, 0); + + var recipe = await _recipeRepository.CreateRecipeForCategoryAsync( + categoryEntity, + model.Name, + model.Description ?? string.Empty, + model.Difficulty, + model.Servings, + model.PreparationTime, + model.CookingTime); + + if (model.Photo != null) + { + await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, model.Photo); + } + + if (model.Video != null) + { + await _mediaFileRepository.UploadRecipeMediaAsync(recipe.Id, model.Video); + } + + if (model.IngredientViewModel?.Ingredients != null) + { + foreach (var ingredient in model.IngredientViewModel.Ingredients) + { + var ri = ingredient.RecipeIngredients?.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(ingredient.Name) && ri?.Quantity > 0 && ri?.Unit?.Id != null) + { + await _recipeRepository.CreateRecipeIngredientAsync( + recipe.Id, + ingredient.Name, + ri.Quantity, + ri.Unit.Id); + } + } + } + + if (model.InstructionViewModel?.Instructions != null) + { + for (var i = 0; i < model.InstructionViewModel.Instructions.Count; i++) + { + var instruction = model.InstructionViewModel.Instructions[i]; + if (!string.IsNullOrWhiteSpace(instruction.Description)) + { + var fileKey = $"InstructionViewModel.Instructions[{i}].MediaFile"; + IFormFile? imageFile = null; + + if (Request.Form.Files.Any(f => f.Name == fileKey)) + { + imageFile = Request.Form.Files[fileKey]; + } + + await _instructionRepository.CreateInstructionAsync( + recipe.Id, + instruction.Description, + imageFile); + } + } + } + + await transaction.CommitAsync(); + return RedirectToAction("Details", new { recipeId = recipe.Id }); + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + return BadRequest(ex.Message); + } + } + + // 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(); + } + + // DELETE: /Recipe/{recipeId}/RemoveIngredient/{ingredientId} + [HttpDelete("{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/{recipeId}/RemoveInstruction/{instructionId} + [HttpGet("{recipeId}/RemoveInstruction/{instructionId}")] + public async Task RemoveInstruction(Guid recipeId, Guid instructionId) + { + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + var instruction = recipe.Instructions?.FirstOrDefault(i => i.Id == instructionId); + if (instruction == null) + { + return NotFound("Instruction not found in the specified recipe."); + } + + ViewBag.RecipeId = recipeId; + ViewBag.InstructionId = instructionId; + + return View(); + } + + // DELETE: /Recipe/{recipeId}/RemoveInstruction/{instructionId} + [HttpDelete("{recipeId}/RemoveInstruction/{instructionId}")] + [ValidateAntiForgeryToken] + public async Task RemoveInstructionConfirmed(Guid recipeId, Guid instructionId) + { + try + { + await _instructionRepository.RemoveInstructionFromRecipeAsync(recipeId, instructionId); + TempData["SuccessMessage"] = "Instruction removed successfully."; + return RedirectToAction("Details", new { recipeId }); + } + catch (Exception ex) + { + TempData["ErrorMessage"] = $"An error occurred while removing the instruction: {ex.Message}"; + return RedirectToAction("Details", new { 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."); + } + + var instructions = await _instructionRepository.GetInstructionsOfRecipeAsync(recipeId); + + var viewModel = new InstructionViewModel + { + RecipeId = recipeId, + Instructions = instructions, + }; + + return View(viewModel); + } + + // POST: /Recipe/{recipeId}/AddInstruction + [HttpPost("{recipeId}/AddInstruction")] + [ValidateAntiForgeryToken] + public async Task AddInstruction(Guid recipeId, string description, IFormFile? image) + { + try + { + await _instructionRepository.CreateInstructionAsync(recipeId, description, image); + + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + var instructions = recipe?.Instructions?.ToList() ?? new List(); + + var viewModel = new InstructionViewModel + { + RecipeId = recipeId, + Instructions = instructions, + }; + + return RedirectToAction("AddInstruction", new { recipeId }); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + // 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); + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return PartialView("_FavoriteButton", recipe); + } + + // POST: /Recipe/RemoveFavorite + [HttpPost("RemoveFavorite")] + [ValidateAntiForgeryToken] + public async Task RemoveFavorite(Guid recipeId) + { + await _favoriteRepository.RemoveFavoriteAsync(recipeId); + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return PartialView("_FavoriteButton", recipe); + } + + // GET: /Recipe/{recipeId}/GetIngredients + [HttpGet("{recipeId}/GetIngredients")] + public async Task GetIngredients(Guid recipeId) + { + var recipeIngredients = (await _ingredientRepository.GetIngredientsByRecipeIdAsync(recipeId)).Select(ri => ri.Ingredient).ToList(); + var units = await _unitRepository.GetAllUnitsAsync(); + + var viewModel = new IngredientViewModel + { + RecipeId = recipeId, + Ingredients = recipeIngredients, + Units = units, + }; + + return PartialView("_IngredientsPartial", viewModel); + } + + // GET: /Recipe/{recipeId}/AdjustableIngredients + [HttpGet("{recipeId}/AdjustableIngredients")] + public async Task GetAdjustableIngredients(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return PartialView("_AdjustableIngredientsPartial", recipe); + } + + // GET: /Recipe/{recipeId}/Delete + [HttpGet("{recipeId}/Delete")] + public async Task Delete(Guid recipeId) + { + var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId); + if (recipe == null) + { + return NotFound("Recipe not found."); + } + + return View(recipe); + } + + // DELETE: /Recipe/{recipeId}/Delete + [HttpDelete("{recipeId}/Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(Guid recipeId) + { + var deleted = await _recipeRepository.DeleteRecipeAsync(recipeId); + + if (deleted) + { + TempData["SuccessMessage"] = "Rezept wurde erfolgreich gelöscht."; + return RedirectToAction("Index", "Home"); + } + else + { + TempData["ErrorMessage"] = "Rezept nicht gefunden oder konnte nicht gelöscht werden."; + 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..a1494a7 --- /dev/null +++ b/Francesco.Recipes.World/Controller/ShoppingList/ShoppingListController.cs @@ -0,0 +1,154 @@ +namespace Francesco.Recipes.World.Controllers +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models; + using Francesco.Recipes.World.Repositories.ShoppingList; + using Microsoft.AspNetCore.Mvc; + using Microsoft.EntityFrameworkCore; + + [Route("ShoppingList")] + public class ShoppingListController : Controller + { + private readonly IShoppingListRepository _shoppingListRepository; + 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 }); + } + + [HttpGet("RecipeCount")] + public async Task RecipeCount() + { + var count = await _shoppingListRepository.CountAllRecipeShoppinglistsAsync(); + return Json(new { count }); + } + + // GET: /ShoppingList/Details + [HttpGet("Details")] + public async Task Details() + { + var recipeCount = await _shoppingListRepository.CountAllRecipeShoppinglistsAsync(); + + var recipeIngredientToShoppingListMap = new Dictionary(); + var allEntries = await _context.RecipeIngredientsShoppingLists + .Where(risl => risl.RecipeIngredient != null) + .Select(risl => new + { + RecipeIngredientId = risl.RecipeIngredient.Id, + ShoppingListId = risl.Id, + }) + .ToListAsync(); + + foreach (var entry in allEntries) + { + if (entry.RecipeIngredientId != Guid.Empty && + !recipeIngredientToShoppingListMap.ContainsKey(entry.RecipeIngredientId)) + { + recipeIngredientToShoppingListMap.Add(entry.RecipeIngredientId, entry.ShoppingListId); + } + } + + var recipeInAnyShoppingList = (await _shoppingListRepository.GetAllShoppingListsAsync()) + .SelectMany(sl => sl.RecipeShoppingList.Select(rsl => rsl.Recipe)) + .ToList(); + + var viewModel = new ShoppingListDetailsViewModel + { + RecipeCount = recipeCount, + RecipesInAnyShoppingList = recipeInAnyShoppingList, + RecipeIngredientToShoppingListMap = recipeIngredientToShoppingListMap, + }; + + return View(viewModel); + } + + [HttpDelete("RemoveIngredients")] + [ValidateAntiForgeryToken] + public async Task RemoveIngredients([FromBody] List recipeIngredientShoppingListIds) + { + if (recipeIngredientShoppingListIds == null || !recipeIngredientShoppingListIds.Any()) + { + return BadRequest("Keine Zutaten zum Entfernen angegeben."); + } + + try + { + var affectedRecipeIds = await _context.RecipeIngredientsShoppingLists + .Where(risl => recipeIngredientShoppingListIds.Contains(risl.Id)) + .Select(risl => risl.RecipeShoppingList.Recipe.Id) + .Distinct() + .ToListAsync(); + + await _shoppingListRepository.RemoveIngredientsFromShoppingListAsync(recipeIngredientShoppingListIds); + + var remainingRecipeIds = await _context.RecipeShoppingLists + .Select(rsl => rsl.Recipe.Id) + .ToListAsync(); + + var removedRecipeIds = affectedRecipeIds + .Where(id => !remainingRecipeIds.Contains(id)) + .ToList(); + + return Json(new { success = true, removedRecipeIds }); + } + catch (Exception ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + [HttpDelete("RemoveRecipeFromList/{recipeId}")] + [ValidateAntiForgeryToken] + public async Task RemoveRecipeFromList(Guid recipeId) + { + try + { + var recipeShoppingLists = await _context.RecipeShoppingLists + .Where(rsl => rsl.Recipe.Id == recipeId) + .ToListAsync(); + + if (!recipeShoppingLists.Any()) + { + return NotFound($"Kein Einkaufslisten-Eintrag für Rezept mit ID {recipeId} gefunden."); + } + + foreach (var recipeShoppingList in recipeShoppingLists) + { + await _shoppingListRepository.RemoveRecipeFromShoppingListAsync(recipeShoppingList.Id); + } + + return Ok(new { success = true }); + } + catch (Exception ex) + { + return BadRequest(new { success = false, error = ex.Message }); + } + } + } +} diff --git a/Francesco.Recipes.World/Controller/Unit/UnitController.cs b/Francesco.Recipes.World/Controller/Unit/UnitController.cs new file mode 100644 index 0000000..d01ffd8 --- /dev/null +++ b/Francesco.Recipes.World/Controller/Unit/UnitController.cs @@ -0,0 +1,25 @@ +namespace Francesco.Recipes.World.Controller.Unit +{ + using Francesco.Recipes.World.Repositories.Unit; + using Microsoft.AspNetCore.Mvc; + + [Route("Unit")] + public class UnitController : Controller + { + private readonly IUnitRepository _unitRepository; + + public UnitController(IUnitRepository unitRepository) + { + _unitRepository = unitRepository; + } + + [HttpGet("GetAllUnits")] + public async Task GetAllUnits() + { + var units = (await _unitRepository.GetAllUnitsAsync()) + .Select(u => new { id = u.Id, name = u.Name }) + .ToList(); + return Json(units); + } + } +} diff --git a/Francesco.Recipes.World/Controllers/HomeController.cs b/Francesco.Recipes.World/Controllers/HomeController.cs deleted file mode 100644 index b5b0244..0000000 --- a/Francesco.Recipes.World/Controllers/HomeController.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Francesco.Recipes.World.Controllers -{ - using System.Diagnostics; - using Francesco.Recipes.World.Models; - using Microsoft.AspNetCore.Mvc; - - public class HomeController : Controller - { - private readonly ILogger _logger; - - public HomeController(ILogger logger) - { - _logger = logger; - } - - public IActionResult Index() - { - return View(); - } - - public IActionResult Privacy() - { - return View(); - } - - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } - } -} diff --git a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs index fe4646e..7a8a5e7 100644 --- a/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs +++ b/Francesco.Recipes.World/Data/FrancescosRecipesWorldDbContext.cs @@ -1,25 +1,119 @@ -namespace Francesco.Recipes.World.Data -{ +namespace Francesco.Recipes.World.Data; + + using Francesco.Recipes.World.Models.BackendModels; using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Favorit; using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; using Francesco.Recipes.World.Models.BackendModels.Instruction; + using Francesco.Recipes.World.Models.BackendModels.MediaFile; using Francesco.Recipes.World.Models.BackendModels.Recipe; using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; +using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; +using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; using Francesco.Recipes.World.Models.BackendModels.Unit; using Microsoft.EntityFrameworkCore; public class FrancescosRecipesWorldDbContext : DbContext { - public FrancescosRecipesWorldDbContext(DbContextOptions options) - : base(options) - { + public FrancescosRecipesWorldDbContext(DbContextOptions options) + : base(options) + { } public DbSet Categories => Set(); + public DbSet Ingredients => Set(); + public DbSet Instructions => Set(); + public DbSet Recipes => Set(); + public DbSet RecipeIngredients => Set(); + public DbSet Units => Set(); + + public DbSet Favorits => Set(); + + public DbSet RecipeIngredientsShoppingLists => Set(); + + public DbSet RecipeShoppingLists => Set(); + + public DbSet ShoppingLists => Set(); + + public DbSet MediaFiles => Set(); + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + foreach (var entry in ChangeTracker.Entries()) + { + if (entry.Entity is ITimeStampedEntity timeStampedEntity) + { + switch (entry.State) + { + case EntityState.Added: + timeStampedEntity.CreatedAt = DateTime.UtcNow; + break; + case EntityState.Modified: + timeStampedEntity.ModifiedAt = DateTime.UtcNow; + break; + } + } + } + + return base.SaveChangesAsync(cancellationToken); + } + + protected override void OnModelCreating(ModelBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + SeedData(builder); + + base.OnModelCreating(builder); + } + + private static void SeedData(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasData(GetCategories()); + + modelBuilder.Entity() + .HasData(GetUnits()); + } + + private static IEnumerable GetCategories() + { + return + [ + new Category { Id = Guid.Parse("b248244f-f21c-4555-a14d-5dd49a2717cf"), Name = "Vorspeisen & Snacks" }, + new Category { Id = Guid.Parse("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), Name = "Erste Gänge" }, + new Category { Id = Guid.Parse("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), Name = "Hauptgerichte" }, + new Category { Id = Guid.Parse("90deec39-dcd0-422d-9018-ac8389e332e1"), Name = "Desserts & Süßspeisen" }, + new Category { Id = Guid.Parse("28e39168-701a-4084-81da-d96c987c462f"), Name = "Beilagen & Salate" }, + new Category { Id = Guid.Parse("20585b74-4805-4aff-a6df-aa6b7af04ff1"), Name = "Kuchen" }, + new Category { Id = Guid.Parse("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), Name = "Hefegebäck & Brot" }, + new Category { Id = Guid.Parse("d332f88d-d241-48c5-a2f2-bfd124eada7e"), Name = "Soßen & Saucen" }, + new Category { Id = Guid.Parse("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), Name = "Marmeladen & Eingemachtes" }, + new Category { Id = Guid.Parse("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), Name = "Getränke" }, + ]; + } + + private static IEnumerable GetUnits() + { + return + [ + new Unit { Id = Guid.Parse("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), Name = "liter", Symbol = "l" }, + new Unit { Id = Guid.Parse("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), Name = "gramm", Symbol = "g" }, + new Unit { Id = Guid.Parse("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), Name = "kilogramm", Symbol = "kg" }, + new Unit { Id = Guid.Parse("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), Name = "stücke", Symbol = "stk" }, + new Unit { Id = Guid.Parse("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), Name = "blatt", Symbol = "blatt" }, + new Unit { Id = Guid.Parse("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), Name = "messerspitze", Symbol = "msp" }, + new Unit { Id = Guid.Parse("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), Name = "stange", Symbol = "stange" }, + new Unit { Id = Guid.Parse("7ea2f51d-7493-4f19-a663-1f309186d3ae"), Name = "bund", Symbol = "bund" }, + new Unit { Id = Guid.Parse("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), Name = "zehe", Symbol = "zehe" }, + new Unit { Id = Guid.Parse("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), Name = "teelöffel", Symbol = "TL" }, + new Unit { Id = Guid.Parse("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), Name = "esslöffel", Symbol = "EL" }, + ]; + } } -} diff --git a/Francesco.Recipes.World/Francesco.Recipes.World.csproj b/Francesco.Recipes.World/Francesco.Recipes.World.csproj index f9e0493..28aea44 100644 --- a/Francesco.Recipes.World/Francesco.Recipes.World.csproj +++ b/Francesco.Recipes.World/Francesco.Recipes.World.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -6,10 +6,14 @@ enable be6a70eb-b657-40b2-b4a1-418e5c6ec131 + + true + $(SolutionDir)Francesco.Recipes.World.ruleset + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,6 +29,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -37,5 +42,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + + + diff --git a/Francesco.Recipes.World/Migrations/20250114131454_InitialMigration.Designer.cs b/Francesco.Recipes.World/Migrations/20250114131454_InitialMigration.Designer.cs deleted file mode 100644 index 77d228f..0000000 --- a/Francesco.Recipes.World/Migrations/20250114131454_InitialMigration.Designer.cs +++ /dev/null @@ -1,230 +0,0 @@ -// -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("20250114131454_InitialMigration")] - partial class InitialMigration - { - /// - 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"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Quantity") - .HasColumnType("int"); - - b.Property("UnitId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("Id"); - - b.HasIndex("UnitId"); - - b.ToTable("Ingredients"); - }); - - 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") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("RecipeId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("Id"); - - b.HasIndex("RecipeId"); - - b.ToTable("Instructions"); - }); - - 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("Description") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Difficulty") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("PreparationTime") - .HasColumnType("time"); - - b.Property("Servings") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("CategoryId"); - - 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("RecipeId") - .HasColumnType("uniqueidentifier"); - - b.HasKey("Id"); - - b.HasIndex("IngredientId"); - - b.HasIndex("RecipeId"); - - b.ToTable("RecipeIngredients"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Unit"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit") - .WithMany("Recipes") - .HasForeignKey("UnitId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Unit"); - }); - - 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.Recipe.Recipe", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Category.Category", null) - .WithMany("Recipes") - .HasForeignKey("CategoryId"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => - { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient") - .WithMany() - .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.Navigation("Ingredient"); - - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => - { - b.Navigation("Recipes"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => - { - b.Navigation("Instructions"); - - b.Navigation("RecipeIngredients"); - }); - - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => - { - b.Navigation("Recipes"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Francesco.Recipes.World/Migrations/20250114131454_InitialMigration.cs b/Francesco.Recipes.World/Migrations/20250114131454_InitialMigration.cs deleted file mode 100644 index b90b0ed..0000000 --- a/Francesco.Recipes.World/Migrations/20250114131454_InitialMigration.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Francesco.Recipes.World.Migrations -{ - /// - public partial class InitialMigration : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - { - throw new ArgumentNullException(nameof(migrationBuilder)); - } - - migrationBuilder.CreateTable( - name: "Categories", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Name = table.Column(type: "nvarchar(max)", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Categories", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Unit", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Name = table.Column(type: "nvarchar(max)", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Unit", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Recipes", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Name = table.Column(type: "nvarchar(max)", nullable: false), - Description = table.Column(type: "nvarchar(max)", nullable: false), - Difficulty = table.Column(type: "nvarchar(max)", nullable: false), - Servings = table.Column(type: "int", nullable: false), - PreparationTime = table.Column(type: "time", nullable: false), - CookingTime = table.Column(type: "time", nullable: false), - CategoryId = table.Column(type: "uniqueidentifier", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Recipes", x => x.Id); - table.ForeignKey( - name: "FK_Recipes_Categories_CategoryId", - column: x => x.CategoryId, - principalTable: "Categories", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "Ingredients", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Name = table.Column(type: "nvarchar(max)", nullable: false), - UnitId = table.Column(type: "uniqueidentifier", nullable: false), - Quantity = table.Column(type: "int", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Ingredients", x => x.Id); - table.ForeignKey( - name: "FK_Ingredients_Unit_UnitId", - column: x => x.UnitId, - principalTable: "Unit", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Instructions", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Description = table.Column(type: "nvarchar(max)", nullable: false), - Number = table.Column(type: "nvarchar(max)", nullable: false), - RecipeId = table.Column(type: "uniqueidentifier", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Instructions", x => x.Id); - table.ForeignKey( - name: "FK_Instructions_Recipes_RecipeId", - column: x => x.RecipeId, - principalTable: "Recipes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "RecipeIngredients", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - RecipeId = table.Column(type: "uniqueidentifier", nullable: false), - IngredientId = table.Column(type: "uniqueidentifier", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_RecipeIngredients", x => x.Id); - table.ForeignKey( - name: "FK_RecipeIngredients_Ingredients_IngredientId", - column: x => x.IngredientId, - principalTable: "Ingredients", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_RecipeIngredients_Recipes_RecipeId", - column: x => x.RecipeId, - principalTable: "Recipes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Ingredients_UnitId", - table: "Ingredients", - column: "UnitId"); - - migrationBuilder.CreateIndex( - name: "IX_Instructions_RecipeId", - table: "Instructions", - column: "RecipeId"); - - migrationBuilder.CreateIndex( - name: "IX_RecipeIngredients_IngredientId", - table: "RecipeIngredients", - column: "IngredientId"); - - migrationBuilder.CreateIndex( - name: "IX_RecipeIngredients_RecipeId", - table: "RecipeIngredients", - column: "RecipeId"); - - migrationBuilder.CreateIndex( - name: "IX_Recipes_CategoryId", - table: "Recipes", - column: "CategoryId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - if (migrationBuilder == null) - { - throw new ArgumentNullException(nameof(migrationBuilder)); - } - - migrationBuilder.DropTable( - name: "Instructions"); - - migrationBuilder.DropTable( - name: "RecipeIngredients"); - - migrationBuilder.DropTable( - name: "Ingredients"); - - migrationBuilder.DropTable( - name: "Recipes"); - - migrationBuilder.DropTable( - name: "Unit"); - - migrationBuilder.DropTable( - name: "Categories"); - } - } -} diff --git a/Francesco.Recipes.World/Migrations/20250318150949_InitialMIgration.Designer.cs b/Francesco.Recipes.World/Migrations/20250318150949_InitialMIgration.Designer.cs new file mode 100644 index 0000000..3f89444 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250318150949_InitialMIgration.Designer.cs @@ -0,0 +1,513 @@ +// +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("20250318150949_InitialMIgration")] + partial class InitialMIgration + { + /// + 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.IngredientsShoppingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IngredientId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShoppinglistId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("ShoppinglistId"); + + b.ToTable("IngredientsShoppingLists"); + }); + + 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") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + 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.IngredientsShoppingList", b => + { + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient") + .WithMany("IngredientShoppingLists") + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Francesco.Recipes.World.Models.BackendModels.Shoppinglist.ShoppingList", "Shoppinglist") + .WithMany("IngredientsShoppingLists") + .HasForeignKey("ShoppinglistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + 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("IngredientsShoppingLists"); + }); + + 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/20250318150949_InitialMIgration.cs b/Francesco.Recipes.World/Migrations/20250318150949_InitialMIgration.cs new file mode 100644 index 0000000..18ff0ed --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250318150949_InitialMIgration.cs @@ -0,0 +1,346 @@ +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Francesco.Recipes.World.Migrations +{ + using System; + using Microsoft.EntityFrameworkCore.Migrations; + + /// + public partial class InitialMIgration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Favorits", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_Favorits", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Ingredients", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_Ingredients", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ShoppingLists", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ModifiedAt = table.Column(type: "datetime2", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingLists", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Units", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Symbol = table.Column(type: "nvarchar(max)", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_Units", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Recipes", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: true), + Difficulty = table.Column(type: "int", nullable: false), + Servings = table.Column(type: "int", nullable: false), + PreparationTime = table.Column(type: "time", nullable: false), + CookingTime = table.Column(type: "time", nullable: false), + IsFavorite = table.Column(type: "bit", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ModifiedAt = table.Column(type: "datetime2", nullable: true), + FavoritId = table.Column(type: "uniqueidentifier", nullable: false), + CategoryId = table.Column(type: "uniqueidentifier", nullable: true), + }, + constraints: table => + { + table.PrimaryKey("PK_Recipes", x => x.Id); + table.ForeignKey( + name: "FK_Recipes_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Recipes_Favorits_FavoritId", + column: x => x.FavoritId, + principalTable: "Favorits", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "IngredientsShoppingLists", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ShoppinglistId = table.Column(type: "uniqueidentifier", nullable: false), + IngredientId = 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.CreateTable( + name: "Instructions", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + Number = table.Column(type: "nvarchar(max)", nullable: false), + RecipeId = table.Column(type: "uniqueidentifier", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_Instructions", x => x.Id); + table.ForeignKey( + name: "FK_Instructions_Recipes_RecipeId", + column: x => x.RecipeId, + principalTable: "Recipes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RecipeIngredients", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + RecipeId = table.Column(type: "uniqueidentifier", nullable: false), + IngredientId = table.Column(type: "uniqueidentifier", nullable: false), + UnitId = table.Column(type: "uniqueidentifier", nullable: false), + Quantity = table.Column(type: "int", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_RecipeIngredients", x => x.Id); + table.ForeignKey( + name: "FK_RecipeIngredients_Ingredients_IngredientId", + column: x => x.IngredientId, + principalTable: "Ingredients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RecipeIngredients_Recipes_RecipeId", + column: x => x.RecipeId, + principalTable: "Recipes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RecipeIngredients_Units_UnitId", + column: x => x.UnitId, + principalTable: "Units", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MediaFiles", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + FileName = table.Column(type: "nvarchar(max)", nullable: true), + MimeType = table.Column(type: "nvarchar(max)", nullable: true), + Data = table.Column(type: "varbinary(max)", nullable: true), + RecipeId = table.Column(type: "uniqueidentifier", nullable: true), + InstructionId = table.Column(type: "uniqueidentifier", nullable: false), + }, + constraints: table => + { + table.PrimaryKey("PK_MediaFiles", x => x.Id); + table.ForeignKey( + name: "FK_MediaFiles_Instructions_InstructionId", + column: x => x.InstructionId, + principalTable: "Instructions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MediaFiles_Recipes_RecipeId", + column: x => x.RecipeId, + principalTable: "Recipes", + principalColumn: "Id"); + }); + + migrationBuilder.InsertData( + table: "Categories", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { new Guid("0a91a200-dc76-4e00-b38c-b38cab5b69d7"), "Getränke" }, + { new Guid("20585b74-4805-4aff-a6df-aa6b7af04ff1"), "Kuchen" }, + { new Guid("23b1c740-e427-44f9-a6ea-d33d3f30f05a"), "Marmeladen & Eingemachtes" }, + { new Guid("28e39168-701a-4084-81da-d96c987c462f"), "Beilagen & Salate" }, + { new Guid("5186c5d6-5aff-4a6e-baf4-b61b72d889fe"), "Erste Gänge" }, + { new Guid("90deec39-dcd0-422d-9018-ac8389e332e1"), "Desserts & Süßspeisen" }, + { new Guid("abbc6bb0-97ea-49f5-b31e-6507eb784fd1"), "Hauptgerichte" }, + { new Guid("adfb75ce-3ef3-428b-a5ca-b0c4c619d5ec"), "Hefegebäck & Brot" }, + { new Guid("b248244f-f21c-4555-a14d-5dd49a2717cf"), "Vorspeisen & Snacks" }, + { new Guid("d332f88d-d241-48c5-a2f2-bfd124eada7e"), "Soßen & Saucen" }, + }); + + migrationBuilder.InsertData( + table: "Units", + columns: new[] { "Id", "Name", "Symbol" }, + values: new object[,] + { + { new Guid("0c3648ec-4981-42c8-abf8-18bf1a2ff4c2"), "stücke", "stk" }, + { new Guid("24a91b89-3389-4465-89e4-2f70d2ea6fd7"), "esslöffel", "EL" }, + { new Guid("2e9894ac-14fa-43fd-ba07-35cdd8ebd461"), "teelöffel", "TL" }, + { new Guid("62eb2c11-8768-46fb-9db0-c77f940bb4aa"), "liter", "l" }, + { new Guid("66556e0e-eb2b-4bc3-9a56-135dd508ed09"), "zehe", "zehe" }, + { new Guid("6c41f7e6-ca75-49cc-8541-cebd5a9c560b"), "gramm", "g" }, + { new Guid("7e3d1b86-ac48-45d6-814e-d3492a86db1d"), "kilogramm", "kg" }, + { new Guid("7ea2f51d-7493-4f19-a663-1f309186d3ae"), "bund", "bund" }, + { new Guid("d82f4abd-e4e4-4104-a9c0-1acdeaa701f5"), "blatt", "blatt" }, + { new Guid("df5cb4c3-4de6-4c6f-be8c-da41b2986408"), "messerspitze", "msp" }, + { new Guid("e45a3af2-2ed6-4ac4-b06f-b4175663a7be"), "stange", "stange" }, + }); + + migrationBuilder.CreateIndex( + name: "IX_IngredientsShoppingLists_IngredientId", + table: "IngredientsShoppingLists", + column: "IngredientId"); + + migrationBuilder.CreateIndex( + name: "IX_IngredientsShoppingLists_ShoppinglistId", + table: "IngredientsShoppingLists", + column: "ShoppinglistId"); + + migrationBuilder.CreateIndex( + name: "IX_Instructions_RecipeId", + table: "Instructions", + column: "RecipeId"); + + migrationBuilder.CreateIndex( + name: "IX_MediaFiles_InstructionId", + table: "MediaFiles", + column: "InstructionId"); + + migrationBuilder.CreateIndex( + name: "IX_MediaFiles_RecipeId", + table: "MediaFiles", + column: "RecipeId"); + + migrationBuilder.CreateIndex( + name: "IX_RecipeIngredients_IngredientId", + table: "RecipeIngredients", + column: "IngredientId"); + + migrationBuilder.CreateIndex( + name: "IX_RecipeIngredients_RecipeId", + table: "RecipeIngredients", + column: "RecipeId"); + + migrationBuilder.CreateIndex( + name: "IX_RecipeIngredients_UnitId", + table: "RecipeIngredients", + column: "UnitId"); + + migrationBuilder.CreateIndex( + name: "IX_Recipes_CategoryId", + table: "Recipes", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Recipes_FavoritId", + table: "Recipes", + column: "FavoritId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropTable( + name: "IngredientsShoppingLists"); + + migrationBuilder.DropTable( + name: "MediaFiles"); + + migrationBuilder.DropTable( + name: "RecipeIngredients"); + + migrationBuilder.DropTable( + name: "ShoppingLists"); + + migrationBuilder.DropTable( + name: "Instructions"); + + migrationBuilder.DropTable( + name: "Ingredients"); + + migrationBuilder.DropTable( + name: "Units"); + + migrationBuilder.DropTable( + name: "Recipes"); + + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "Favorits"); + } + } +} 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..e98a564 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250324124459_CreateNewTableRecipeIngredientShoppinglist.Designer.cs @@ -0,0 +1,518 @@ +// +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("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/20250430094852_MakeInstructionOptionalInMediaFile.Designer.cs b/Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.Designer.cs new file mode 100644 index 0000000..6344cb6 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.Designer.cs @@ -0,0 +1,571 @@ +// +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("20250430094852_MakeInstructionOptionalInMediaFile")] + partial class MakeInstructionOptionalInMediaFile + { + /// + 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"); + + 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("RecipeShoppingList") + .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("RecipeShoppingList"); + }); + + 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/20250430094852_MakeInstructionOptionalInMediaFile.cs b/Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.cs new file mode 100644 index 0000000..b6fb4f7 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250430094852_MakeInstructionOptionalInMediaFile.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + /// + public partial class MakeInstructionOptionalInMediaFile : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_MediaFiles_Instructions_InstructionId", + table: "MediaFiles"); + + migrationBuilder.AlterColumn( + name: "InstructionId", + table: "MediaFiles", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + + migrationBuilder.AddForeignKey( + name: "FK_MediaFiles_Instructions_InstructionId", + table: "MediaFiles", + column: "InstructionId", + principalTable: "Instructions", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_MediaFiles_Instructions_InstructionId", + table: "MediaFiles"); + + migrationBuilder.AlterColumn( + name: "InstructionId", + table: "MediaFiles", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_MediaFiles_Instructions_InstructionId", + table: "MediaFiles", + column: "InstructionId", + principalTable: "Instructions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs new file mode 100644 index 0000000..b7ba81b --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.Designer.cs @@ -0,0 +1,571 @@ +// +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("20250602133408_RenameFavoriteColumn")] + partial class RenameFavoriteColumn + { + /// + 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("FavoriteId") + .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("FavoriteId"); + + 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"); + + 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", "Favorite") + .WithMany("Recipe") + .HasForeignKey("FavoriteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Favorite"); + }); + + 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("RecipeShoppingList") + .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("RecipeShoppingList"); + }); + + 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/20250602133408_RenameFavoriteColumn.cs b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.cs new file mode 100644 index 0000000..79582f6 --- /dev/null +++ b/Francesco.Recipes.World/Migrations/20250602133408_RenameFavoriteColumn.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Francesco.Recipes.World.Migrations +{ + /// + public partial class RenameFavoriteColumn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_Recipes_Favorits_FavoritId", + table: "Recipes"); + + migrationBuilder.RenameColumn( + name: "FavoritId", + table: "Recipes", + newName: "FavoriteId"); + + migrationBuilder.RenameIndex( + name: "IX_Recipes_FavoritId", + table: "Recipes", + newName: "IX_Recipes_FavoriteId"); + + migrationBuilder.AddForeignKey( + name: "FK_Recipes_Favorits_FavoriteId", + table: "Recipes", + column: "FavoriteId", + principalTable: "Favorits", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + if (migrationBuilder is null) + { + throw new ArgumentNullException(nameof(migrationBuilder)); + } + + migrationBuilder.DropForeignKey( + name: "FK_Recipes_Favorits_FavoriteId", + table: "Recipes"); + + migrationBuilder.RenameColumn( + name: "FavoriteId", + table: "Recipes", + newName: "FavoritId"); + + migrationBuilder.RenameIndex( + name: "IX_Recipes_FavoriteId", + table: "Recipes", + newName: "IX_Recipes_FavoritId"); + + migrationBuilder.AddForeignKey( + name: "FK_Recipes_Favorits_FavoritId", + table: "Recipes", + column: "FavoritId", + principalTable: "Favorits", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs index 309c9b2..67067f0 100644 --- a/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs +++ b/Francesco.Recipes.World/Migrations/FrancescosRecipesWorldDbContextModelSnapshot.cs @@ -35,6 +35,72 @@ namespace Francesco.Recipes.World.Migrations 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 => @@ -47,17 +113,38 @@ namespace Francesco.Recipes.World.Migrations .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("Quantity") - .HasColumnType("int"); + b.HasKey("Id"); - b.Property("UnitId") + 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("UnitId"); + b.HasIndex("IngredientId"); - b.ToTable("Ingredients"); + b.HasIndex("RecipeIngredientId"); + + b.HasIndex("RecipeShoppingListId"); + + b.ToTable("RecipeIngredientsShoppingLists"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Instruction.Instruction", b => @@ -70,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"); @@ -84,25 +170,65 @@ namespace Francesco.Recipes.World.Migrations 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") + b.Property("CategoryId") .HasColumnType("uniqueidentifier"); b.Property("CookingTime") .HasColumnType("time"); + b.Property("CreatedAt") + .HasColumnType("datetime2"); + b.Property("Description") - .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("Difficulty") - .IsRequired() - .HasColumnType("nvarchar(max)"); + b.Property("Difficulty") + .HasColumnType("int"); + + b.Property("FavoriteId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsFavorite") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetime2"); b.Property("Name") .IsRequired() @@ -118,6 +244,8 @@ namespace Francesco.Recipes.World.Migrations b.HasIndex("CategoryId"); + b.HasIndex("FavoriteId"); + b.ToTable("Recipes"); }); @@ -130,18 +258,64 @@ namespace Francesco.Recipes.World.Migrations 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") @@ -152,20 +326,104 @@ namespace Francesco.Recipes.World.Migrations .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("Symbol") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.HasKey("Id"); - b.ToTable("Unit"); + 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.Ingredient.Ingredient", b => + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList.RecipeIngredientShoppingList", b => { - b.HasOne("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", "Unit") - .WithMany("Recipes") - .HasForeignKey("UnitId") + 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.Navigation("Unit"); + 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 => @@ -179,17 +437,44 @@ namespace Francesco.Recipes.World.Migrations 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"); + + 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) + 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", "Favorite") + .WithMany("Recipe") + .HasForeignKey("FavoriteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Favorite"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.RecipeIngredient.RecipeIngredient", b => { b.HasOne("Francesco.Recipes.World.Models.BackendModels.Ingredient.Ingredient", "Ingredient") - .WithMany() + .WithMany("RecipeIngredients") .HasForeignKey("IngredientId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -200,9 +485,36 @@ namespace Francesco.Recipes.World.Migrations .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("RecipeShoppingList") + .HasForeignKey("ShoppingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + + b.Navigation("ShoppingList"); }); modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Category.Category", b => @@ -210,16 +522,45 @@ namespace Francesco.Recipes.World.Migrations b.Navigation("Recipes"); }); - modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe", b => + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Favorit.Favorit", b => { - b.Navigation("Instructions"); + 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("RecipeShoppingList"); + }); + modelBuilder.Entity("Francesco.Recipes.World.Models.BackendModels.Unit.Unit", b => { - b.Navigation("Recipes"); + b.Navigation("RecipeIngredient"); }); #pragma warning restore 612, 618 } diff --git a/Francesco.Recipes.World/Models/BackendModels/Category/Category.cs b/Francesco.Recipes.World/Models/BackendModels/Category/Category.cs index 5fa42d4..b088952 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Category/Category.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Category/Category.cs @@ -1,10 +1,13 @@ namespace Francesco.Recipes.World.Models.BackendModels.Category { using Francesco.Recipes.World.Models.BackendModels.Recipe; + public class Category { public Guid Id { get; set; } - public string Name { get; set; } + + public string Name { get; set; } = string.Empty; + public virtual ICollection Recipes { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Models/BackendModels/Favorit/Favorit.cs b/Francesco.Recipes.World/Models/BackendModels/Favorit/Favorit.cs new file mode 100644 index 0000000..97e12ed --- /dev/null +++ b/Francesco.Recipes.World/Models/BackendModels/Favorit/Favorit.cs @@ -0,0 +1,13 @@ +namespace Francesco.Recipes.World.Models.BackendModels.Favorit +{ + using Francesco.Recipes.World.Models.BackendModels.Recipe; + + public class Favorit + { + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public virtual ICollection Recipe { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Models/BackendModels/ITimeStampedEntity.cs b/Francesco.Recipes.World/Models/BackendModels/ITimeStampedEntity.cs new file mode 100644 index 0000000..baa9f35 --- /dev/null +++ b/Francesco.Recipes.World/Models/BackendModels/ITimeStampedEntity.cs @@ -0,0 +1,9 @@ +namespace Francesco.Recipes.World.Models.BackendModels +{ + public interface ITimeStampedEntity + { + DateTime CreatedAt { get; set; } + + DateTime? ModifiedAt { get; set; } + } +} diff --git a/Francesco.Recipes.World/Models/BackendModels/Ingredient/Ingredient.cs b/Francesco.Recipes.World/Models/BackendModels/Ingredient/Ingredient.cs index 96f377f..ccc9ba1 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Ingredient/Ingredient.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Ingredient/Ingredient.cs @@ -1,11 +1,16 @@ namespace Francesco.Recipes.World.Models.BackendModels.Ingredient { - using Francesco.Recipes.World.Models.BackendModels.Unit; + using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; + using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; + public class Ingredient { public Guid Id { get; set; } - public string Name { get; set; } - public virtual Unit Unit { get; set; } = new(); - public int Quantity { get; set; } + + public string Name { get; set; } = string.Empty; + + public virtual ICollection RecipeIngredients { get; set; } = new List(); + + public virtual ICollection IngredientShoppingLists { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Models/BackendModels/Instruction/Instruction.cs b/Francesco.Recipes.World/Models/BackendModels/Instruction/Instruction.cs index 36a2bd1..77a16d1 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Instruction/Instruction.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Instruction/Instruction.cs @@ -1,11 +1,18 @@ namespace Francesco.Recipes.World.Models.BackendModels.Instruction { + using Francesco.Recipes.World.Models.BackendModels.MediaFile; using Francesco.Recipes.World.Models.BackendModels.Recipe; + public class Instruction { public Guid Id { get; set; } - public string Description { get; set; } - public string Number { get; set; } - public virtual Recipe Recipe { get; set; } = new(); + + public string Description { get; set; } = string.Empty; + + public int Number { get; set; } + + public virtual Recipe Recipe { get; set; } = new (); + + public virtual ICollection MediaFiles { get; set; } = new List(); } } diff --git a/Francesco.Recipes.World/Models/BackendModels/MediaFile/MediaFile.cs b/Francesco.Recipes.World/Models/BackendModels/MediaFile/MediaFile.cs new file mode 100644 index 0000000..6f98b14 --- /dev/null +++ b/Francesco.Recipes.World/Models/BackendModels/MediaFile/MediaFile.cs @@ -0,0 +1,20 @@ +namespace Francesco.Recipes.World.Models.BackendModels.MediaFile +{ + using Francesco.Recipes.World.Models.BackendModels.Instruction; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + + public class MediaFile + { + public Guid Id { get; set; } = Guid.NewGuid(); + + public string? FileName { get; set; } + + public string? MimeType { get; set; } + + public byte[]? Data { get; set; } + + public virtual Recipe? Recipe { get; set; } = null; + + public virtual Instruction? Instruction { get; set; } = null; + } +} diff --git a/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs new file mode 100644 index 0000000..5dc055b --- /dev/null +++ b/Francesco.Recipes.World/Models/BackendModels/Recipe/Difficulty.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace Francesco.Recipes.World.Models.BackendModels.Recipe +{ + public enum Difficulty + { + [Display(Name = "Sehr einfach")] + VeryEasy = 0, + + [Display(Name = "Einfach")] + Easy = 1, + + [Display(Name = "Mittel")] + Medium = 2, + + [Display(Name = "Schwer")] + Hard = 3, + + [Display(Name = "Experte")] + Expert = 4, + } +} diff --git a/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs b/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs index 1712072..9e688a8 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Recipe/Recipe.cs @@ -1,18 +1,40 @@ -namespace Francesco.Recipes.World.Models.BackendModels.Recipe -{ - using Francesco.Recipes.World.Models.BackendModels.Instruction; - using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; +namespace Francesco.Recipes.World.Models.BackendModels.Recipe; - public class Recipe - { - public Guid Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public string Difficulty { get; set; } - public int Servings { get; set; } - public TimeSpan PreparationTime { get; set; } - public TimeSpan CookingTime { get; set; } - public virtual ICollection RecipeIngredients { get; set; } = new List(); - public virtual ICollection Instructions { get; set; } = new List(); - } +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; +using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; + +public class Recipe : ITimeStampedEntity +{ + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public Difficulty Difficulty { get; set; } + + public int Servings { get; set; } + + public TimeSpan PreparationTime { get; set; } + + public TimeSpan CookingTime { get; set; } + + public bool IsFavorite { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? ModifiedAt { get; set; } + + public virtual ICollection RecipeIngredients { get; set; } = new List(); + + public virtual ICollection Instructions { get; set; } = new List(); + + public virtual ICollection MediaFiles { get; set; } = new List(); + + public virtual Favorit Favorite { get; set; } = new (); + + public virtual Category Category { get; set; } = new (); } diff --git a/Francesco.Recipes.World/Models/BackendModels/RecipeIngredient/RecipeIngredient.cs b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredient/RecipeIngredient.cs index 79dea52..d64abb5 100644 --- a/Francesco.Recipes.World/Models/BackendModels/RecipeIngredient/RecipeIngredient.cs +++ b/Francesco.Recipes.World/Models/BackendModels/RecipeIngredient/RecipeIngredient.cs @@ -2,10 +2,18 @@ { using Francesco.Recipes.World.Models.BackendModels.Ingredient; using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Models.BackendModels.Unit; + public class RecipeIngredient { public Guid Id { get; set; } + public virtual Recipe Recipe { get; set; } = new (); + public virtual Ingredient Ingredient { get; set; } = new (); + + public virtual Unit Unit { get; set; } = new (); + + public int Quantity { get; set; } } } 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 new file mode 100644 index 0000000..1a06a05 --- /dev/null +++ b/Francesco.Recipes.World/Models/BackendModels/Shoppinglist/ShoppingList.cs @@ -0,0 +1,15 @@ +namespace Francesco.Recipes.World.Models.BackendModels.Shoppinglist +{ + using Francesco.Recipes.World.Models.BackendModels.RecipeShoppingList; + + public class ShoppingList : ITimeStampedEntity + { + public Guid Id { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? ModifiedAt { get; set; } + + public virtual ICollection RecipeShoppingList { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Models/BackendModels/Unit/Unit.cs b/Francesco.Recipes.World/Models/BackendModels/Unit/Unit.cs index 19ba712..ba8a539 100644 --- a/Francesco.Recipes.World/Models/BackendModels/Unit/Unit.cs +++ b/Francesco.Recipes.World/Models/BackendModels/Unit/Unit.cs @@ -1,11 +1,15 @@ namespace Francesco.Recipes.World.Models.BackendModels.Unit { - using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.RecipeIngredient; public class Unit { public Guid Id { get; set; } - public string Name { get; set; } - public virtual ICollection Recipes { get; set; } = new List(); + + public string Name { get; set; } = string.Empty; + + public string Symbol { get; set; } = string.Empty; + + public virtual ICollection RecipeIngredient { 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/Models/CreateRecipeViewModel.cs b/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs new file mode 100644 index 0000000..bccb60a --- /dev/null +++ b/Francesco.Recipes.World/Models/CreateRecipeViewModel.cs @@ -0,0 +1,39 @@ +using Francesco.Recipes.World.Models.BackendModels.Recipe; + +namespace Francesco.Recipes.World.Models +{ + public class CreateRecipeViewModel + { + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public Difficulty Difficulty { get; set; } + + public int Servings { get; set; } + + public TimeSpan PreparationTime { get; set; } + + public TimeSpan CookingTime { get; set; } + + public int PrepHours { get; set; } + + public int PrepMinutes { get; set; } + + public int CookHours { get; set; } + + public int CookMinutes { get; set; } + + public Guid CategoryId { get; set; } + + public string? CategoryName { get; set; } + + public IFormFile? Photo { get; set; } + + public IFormFile? Video { get; set; } + + public IngredientViewModel? IngredientViewModel { get; set; } + + public InstructionViewModel? InstructionViewModel { get; set; } + } +} diff --git a/Francesco.Recipes.World/Models/FavoriteViewModel.cs b/Francesco.Recipes.World/Models/FavoriteViewModel.cs new file mode 100644 index 0000000..b3412c6 --- /dev/null +++ b/Francesco.Recipes.World/Models/FavoriteViewModel.cs @@ -0,0 +1,16 @@ +using Francesco.Recipes.World.Constants; +using Francesco.Recipes.World.Models.BackendModels.Recipe; + +namespace Francesco.Recipes.World.Models +{ + public class FavoriteViewModel + { + public IEnumerable FavoriteRecipes { get; set; } = new List(); + + public string SortOrder { get; set; } = SortOrders.Newest; + + public bool HasFavorites => FavoriteRecipes.Any(); + + public string SortOrderDisplayText => SortOrder == SortOrders.Oldest ? "Älteste Favorits" : "Neueste Favorits"; + } +} diff --git a/Francesco.Recipes.World/Models/IngredientViewModel.cs b/Francesco.Recipes.World/Models/IngredientViewModel.cs new file mode 100644 index 0000000..bb16514 --- /dev/null +++ b/Francesco.Recipes.World/Models/IngredientViewModel.cs @@ -0,0 +1,14 @@ +namespace Francesco.Recipes.World.Models +{ + using Francesco.Recipes.World.Models.BackendModels.Ingredient; + using Francesco.Recipes.World.Models.BackendModels.Unit; + + public class IngredientViewModel + { + public Guid RecipeId { get; set; } + + public List Ingredients { get; set; } = new List(); + + public List Units { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Models/InstructionViewModel.cs b/Francesco.Recipes.World/Models/InstructionViewModel.cs new file mode 100644 index 0000000..fc5afda --- /dev/null +++ b/Francesco.Recipes.World/Models/InstructionViewModel.cs @@ -0,0 +1,13 @@ +using Francesco.Recipes.World.Models.BackendModels.Instruction; + +namespace Francesco.Recipes.World.Models +{ + public class InstructionViewModel + { + public Guid RecipeId { get; set; } + + public string Description { get; set; } = string.Empty; + + public List Instructions { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Models/SearchViewModel.cs b/Francesco.Recipes.World/Models/SearchViewModel.cs new file mode 100644 index 0000000..c148d5e --- /dev/null +++ b/Francesco.Recipes.World/Models/SearchViewModel.cs @@ -0,0 +1,21 @@ +namespace Francesco.Recipes.World.Models +{ + public class SearchViewModel + { + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public bool IsFavorite { get; set; } + + public byte[]? ImageData { get; set; } + + public string? MimeType { get; set; } + + public List Ingredients { get; set; } = new (); + + public TimeSpan TotalTime { get; set; } + } +} diff --git a/Francesco.Recipes.World/Models/ShoppingListDetailsViewModel.cs b/Francesco.Recipes.World/Models/ShoppingListDetailsViewModel.cs new file mode 100644 index 0000000..10f5152 --- /dev/null +++ b/Francesco.Recipes.World/Models/ShoppingListDetailsViewModel.cs @@ -0,0 +1,13 @@ +using Francesco.Recipes.World.Models.BackendModels.Recipe; + +namespace Francesco.Recipes.World.Models +{ + public class ShoppingListDetailsViewModel + { + public int RecipeCount { get; set; } + + public List RecipesInAnyShoppingList { get; set; } = new List(); + + public Dictionary RecipeIngredientToShoppingListMap { get; set; } = new Dictionary(); + } +} diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index c36c4ac..60f7325 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -1,24 +1,49 @@ using Francesco.Recipes.World.Data; -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 Francesco.Recipes.World.Services.Instruction; using Microsoft.EntityFrameworkCore; -using Serilog; - var builder = WebApplication.CreateBuilder(args); + var services = builder.Services; + var configuration = builder.Configuration; var connectionString = builder.Configuration.GetConnectionString("FrancescosRecipesWorldDbContextConnection") ?? throw new InvalidOperationException("Connection string 'FrancescosRecipesWorldDbContextConnection' not found."); -services.AddDbContext(options => - options.UseSqlServer(connectionString)); -services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) - .AddEntityFrameworkStores(); +services.AddDbContext(options => + options.UseSqlServer(connectionString, sqlOptions => + sqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery))); // Add services to the container. builder.Services.AddControllersWithViews(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); + +builder.Services.AddScoped(); + +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. @@ -30,18 +55,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..627df0d --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Category/CategoryRepository.cs @@ -0,0 +1,89 @@ +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; + using Francesco.Recipes.World.Models.BackendModels.Category; + using Francesco.Recipes.World.Models.BackendModels.Recipe; + using Francesco.Recipes.World.Views.Category; + 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() + { + var categories = await _context.Categories + .Include(c => c.Recipes.OrderByDescending(r => r.CreatedAt).Take(3)) + .ThenInclude(r => r.MediaFiles.Take(2)) + .AsSplitQuery() + .ToListAsync(); + + return categories; + } + + public async Task> GetAllCategoriesWithRecipesViewModelAsync() + { + var categories = await _context.Categories + .Select(c => new CategoryRecipesViewModel + { + Category = c, + Recipes = c.Recipes + .OrderByDescending(r => r.CreatedAt) + .Take(3) + .Select(r => new RecipeCardViewModel + { + Id = r.Id, + Name = r.Name, + CookingTime = r.CookingTime, + IsFavorite = r.IsFavorite, + ImageData = r.MediaFiles + .OrderBy(m => m.Id) + .Select(m => m.Data) + .FirstOrDefault(), + MimeType = r.MediaFiles + .OrderBy(m => m.Id) + .Select(m => m.MimeType) + .FirstOrDefault(), + }), + }) + .AsSplitQuery() + .ToListAsync(); + + return categories; + } + } +} diff --git a/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs b/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs new file mode 100644 index 0000000..3978132 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Category/ICategoryRepository.cs @@ -0,0 +1,22 @@ +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; + using Francesco.Recipes.World.Views.Category; + + public interface ICategoryRepository + { + Task GetCategoryByIdAsync(Guid categoryId); + + Task> GetAllCategoriesAsync(); + + Task> GetRecipesByCategoryAsync(Guid categoryId); + + Task> GetAllCategoriesWithRecipesAsync(); + + Task> GetAllCategoriesWithRecipesViewModelAsync(); + } +} diff --git a/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs new file mode 100644 index 0000000..9c996c1 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Favorit/FavoritRepository.cs @@ -0,0 +1,72 @@ +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) + .Include(r => r.Favorite) + .Include(r => r.MediaFiles) + .Take(6) + .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 + .Include(r => r.Favorite) + .FirstOrDefaultAsync(r => r.Id == recipeId); + + if (recipe == null) + { + throw new InvalidOperationException("Recipe not found."); + } + + if (recipe.IsFavorite) + { + throw new InvalidOperationException("Recipe is already a favorite."); + } + + recipe.IsFavorite = true; + + if (recipe.Favorite == null || recipe.Favorite.Id == Guid.Empty) + { + recipe.Favorite = new Models.BackendModels.Favorit.Favorit + { + Id = Guid.NewGuid(), + CreatedAt = DateTime.Now, + }; + } + + 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..7de7e9e --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs @@ -0,0 +1,19 @@ +namespace Francesco.Recipes.World.Repositories.Instruction +{ + using Francesco.Recipes.World.Models.BackendModels.Instruction; + + public interface IInstructionRepository + { + Task GetInstructionAsync(Guid instructionId); + + Task CreateInstructionAsync(Guid recipeId, string description, IFormFile? photo); + + Task RemoveInstructionFromRecipeAsync(Guid recipeId, Guid instructionId); + + Task SwapInstructionNumbersAsync(Instruction a, Instruction b); + + Task> GetInstructionsOfRecipeAsync(Guid recipeId); + + Task RenumberInstructionsAsync(Guid recipeId); + } +} diff --git a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs new file mode 100644 index 0000000..aed5d2a --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs @@ -0,0 +1,160 @@ +namespace Francesco.Recipes.World.Repositories.Instruction +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.Instruction; + using Francesco.Recipes.World.Models.BackendModels.MediaFile; + 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 CreateInstructionAsync(Guid recipeId, string description, IFormFile? photo) + { + if (string.IsNullOrWhiteSpace(description)) + { + throw new ArgumentException("Description cannot be empty", nameof(description)); + } + + var recipe = await _context.Recipes + .Include(r => r.Instructions) + .FirstOrDefaultAsync(r => r.Id == recipeId); + + if (recipe == null) + { + throw new ArgumentException("Recipe not found.", nameof(recipeId)); + } + + var nextNumber = 1; + if (recipe.Instructions != null && recipe.Instructions.Any()) + { + nextNumber = recipe.Instructions.Max(i => i.Number) + 1; + } + + var newInstruction = new Instruction + { + Id = Guid.NewGuid(), + Description = description, + Number = nextNumber, + Recipe = recipe, + }; + + _context.Instructions.Add(newInstruction); + await _context.SaveChangesAsync(); + + if (photo != null && photo.Length > 0) + { + using var memoryStream = new MemoryStream(); + await photo.CopyToAsync(memoryStream); + + var instructionImage = new MediaFile + { + FileName = photo.FileName, + MimeType = photo.ContentType, + Data = memoryStream.ToArray(), + Instruction = newInstruction, + Recipe = null, + }; + + _context.Add(instructionImage); + 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) + { + await _context.Entry(instructionToRemove) + .Collection(i => i.MediaFiles) + .LoadAsync(); + + if (instructionToRemove.MediaFiles != null && instructionToRemove.MediaFiles.Any()) + { + _context.MediaFiles.RemoveRange(instructionToRemove.MediaFiles); + } + + recipe.Instructions?.Remove(instructionToRemove); + await _context.SaveChangesAsync(); + await RenumberInstructionsAsync(recipeId); + } + } + + public async Task> GetInstructionsOfRecipeAsync(Guid recipeId) + { + var instructions = await _context.Instructions + .Include(i => i.MediaFiles) + .Where(i => i.Recipe.Id == recipeId) + .OrderBy(i => i.Number) + .ToListAsync(); + + if (!instructions.Any()) + { + throw new InvalidDataException($"No instructions found for Recipe ID {recipeId}."); + } + + return instructions; + } + + public async Task RenumberInstructionsAsync(Guid recipeId) + { + var instructions = await _context.Instructions + .Where(i => i.Recipe.Id == recipeId) + .OrderBy(i => i.Number) + .ToListAsync(); + + for (var i = 0; i < instructions.Count; i++) + { + instructions[i].Number = i + 1; + } + + await _context.SaveChangesAsync(); + } + + public async Task SwapInstructionNumbersAsync(Instruction a, Instruction b) + { + if (a == null) + { + throw new ArgumentNullException(nameof(a), "Instruction 'a' cannot be null."); + } + + if (b == null) + { + throw new ArgumentNullException(nameof(b), "Instruction 'b' cannot be null."); + } + + var temp = a.Number; + a.Number = b.Number; + b.Number = temp; + + await _context.SaveChangesAsync(); + } + } +} 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..d929250 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/MediaFile/MediaFileRepository.cs @@ -0,0 +1,136 @@ +namespace Francesco.Recipes.World.Repositories.MediaFile +{ + using Francesco.Recipes.World.Constants; + 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, + Recipe = null, + }; + + _context.Add(instructionImage); + await _context.SaveChangesAsync(); + } + } + + public async Task UploadRecipeMediaAsync(Guid recipeId, IFormFile mediaFile) + { + if (mediaFile == null || mediaFile.Length == 0) + { + return; + } + + try + { + var recipe = await _recipeRepository.GetRecipeAsync(recipeId); + + var isImage = mediaFile.ContentType.StartsWith(ContentType.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, ContentType.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, + Instruction = null, + }; + + _context.MediaFiles.Add(newMedia); + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + throw new InvalidOperationException("An error occurred while uploading the media file.", ex); + } + } + + 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..4fccc61 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Recipe/IRecipeRepository.cs @@ -0,0 +1,25 @@ +namespace Francesco.Recipes.World.Repositories.Recipe +{ + using Francesco.Recipes.World.Models; + 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 CreateRecipeIngredientAsync(Guid recipeId, string ingredientName, int quantity, Guid unitId); + + Task RemoveIngredientFromRecipeAsync(Guid recipeId, Guid ingredientId); + + Task> GetRecipesByDifficultyAsync(Difficulty? difficulty); + + Task CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime); + + Task DeleteRecipeAsync(Guid recipeId); + + Task> SearchInRecipesAndIngredients(string searchTerm); + } +} diff --git a/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs new file mode 100644 index 0000000..2dd0a2b --- /dev/null +++ b/Francesco.Recipes.World/Repositories/Recipe/RecipeRepository.cs @@ -0,0 +1,273 @@ +namespace Francesco.Recipes.World.Repositories.Recipe +{ + using System.Linq.Expressions; + using Francesco.Recipes.World.Constants; + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models; + 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) + .ThenInclude(i => i.MediaFiles) + .FirstOrDefaultAsync(r => r.Id == recipeId); + } + + public async Task CreateRecipeIngredientAsync(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> SearchInRecipesAndIngredients(string searchTerm) + { + try + { + if (string.IsNullOrWhiteSpace(searchTerm)) + { + return await _context.Recipes + .OrderByDescending(r => r.CreatedAt) + .Take(20) + .Select(SearchViewModelSelector()) + .ToListAsync(); + } + + var normalizedSearchTerm = searchTerm.ToLower(); + + return await ApplyRecipeSearchFilter(_context.Recipes, normalizedSearchTerm) + .OrderByDescending(r => r.CreatedAt) + .Take(100) + .Select(SearchViewModelSelector()) + .ToListAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Error in SearchInRecipesAndIngredientsOptimized: {ex.Message}"); + return new List(); + } + } + + 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(); + } + + public async Task DeleteRecipeAsync(Guid recipeId) + { + var recipe = await GetRecipeByIdAsync(recipeId); + + if (recipe == null) + { + return false; + } + + if (recipe.RecipeIngredients != null && recipe.RecipeIngredients.Any()) + { + _context.RecipeIngredients.RemoveRange(recipe.RecipeIngredients); + } + + if (recipe.Instructions != null && recipe.Instructions.Any()) + { + foreach (var instruction in recipe.Instructions) + { + if (instruction.MediaFiles != null && instruction.MediaFiles.Any()) + { + _context.MediaFiles.RemoveRange(instruction.MediaFiles); + } + } + + _context.Instructions.RemoveRange(recipe.Instructions); + } + + if (recipe.MediaFiles != null && recipe.MediaFiles.Any()) + { + _context.MediaFiles.RemoveRange(recipe.MediaFiles); + } + + if (recipe.Favorite != null && recipe.Favorite.Id != Guid.Empty) + { + _context.Remove(recipe.Favorite); + } + + _context.Recipes.Remove(recipe); + + await _context.SaveChangesAsync(); + return true; + } + + private static Expression> SearchViewModelSelector() + { + return r => new SearchViewModel + { + Id = r.Id, + Name = r.Name, + Description = r.Description, + IsFavorite = r.IsFavorite, + ImageData = r.MediaFiles + .Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image)) + .Select(m => m.Data) + .FirstOrDefault(), + MimeType = r.MediaFiles + .Where(m => m.MimeType != null && m.MimeType.StartsWith(ContentType.Image)) + .Select(m => m.MimeType) + .FirstOrDefault(), + Ingredients = r.RecipeIngredients.Select(ri => ri.Ingredient.Name).ToList(), + TotalTime = r.PreparationTime.Add(r.CookingTime), + }; + } + + private static IQueryable ApplyRecipeSearchFilter(IQueryable query, string normalizedSearchTerm) + { + return query.Where(r => + EF.Functions.Like(r.Name.ToLower(), $"%{normalizedSearchTerm}%") || + (r.Description != null && EF.Functions.Like(r.Description.ToLower(), $"%{normalizedSearchTerm}%")) || + r.RecipeIngredients.Any(ri => EF.Functions.Like(ri.Ingredient.Name.ToLower(), $"%{normalizedSearchTerm}%"))); + } + } +} diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs new file mode 100644 index 0000000..63085e4 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/ShoppingList/IShoppingListRepository.cs @@ -0,0 +1,19 @@ +namespace Francesco.Recipes.World.Repositories.ShoppingList +{ + using Francesco.Recipes.World.Models.BackendModels.Shoppinglist; + + public interface IShoppingListRepository + { + Task AddIngredientsToShoppingListAsync(Guid recipeId, List ingredientIds); + + Task RemoveRecipeIfEmptyAsync(Guid shoppingListRecipeId); + + Task CountAllRecipeShoppinglistsAsync(); + + Task> GetAllShoppingListsAsync(); + + Task RemoveIngredientsFromShoppingListAsync(List recipeIngredientShoppngListIds); + + Task RemoveRecipeFromShoppingListAsync(Guid recipeShoppingListId); + } +} diff --git a/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs new file mode 100644 index 0000000..fd814b1 --- /dev/null +++ b/Francesco.Recipes.World/Repositories/ShoppingList/ShoppingListRepository.cs @@ -0,0 +1,235 @@ +namespace Francesco.Recipes.World.Repositories.ShoppingList +{ + using Francesco.Recipes.World.Data; + using Francesco.Recipes.World.Models.BackendModels.IngredientShoppingList; + 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> GetAllShoppingListsAsync() + { + return await GetShoppingListQuery() + .OrderByDescending(sl => sl.CreatedAt) + .ToListAsync(); + } + + public async Task RemoveRecipeIfEmptyAsync(Guid shoppingListRecipeId) + { + var recipeEntry = await _context.RecipeShoppingLists + .Include(r => r.SelectedIngredients) + .Include(r => r.ShoppingList) + .ThenInclude(sl => sl.RecipeShoppingList) + .FirstOrDefaultAsync(r => r.Id == shoppingListRecipeId); + + if (recipeEntry != null && !recipeEntry.SelectedIngredients.Any()) + { + var shoppingList = recipeEntry.ShoppingList; + + _context.RecipeShoppingLists.Remove(recipeEntry); + await _context.SaveChangesAsync(); + + if (shoppingList != null && (shoppingList.RecipeShoppingList == null || !shoppingList.RecipeShoppingList.Any())) + { + _context.ShoppingLists.Remove(shoppingList); + await _context.SaveChangesAsync(); + } + } + } + + public async Task RemoveRecipeFromShoppingListAsync(Guid recipeShoppingListId) + { + var recipeEntry = await _context.RecipeShoppingLists + .Include(r => r.SelectedIngredients) + .Include(r => r.ShoppingList) + .ThenInclude(sl => sl.RecipeShoppingList) + .FirstOrDefaultAsync(r => r.Id == recipeShoppingListId); + + if (recipeEntry != null) + { + var shoppingList = recipeEntry.ShoppingList; + + _context.RecipeIngredientsShoppingLists.RemoveRange(recipeEntry.SelectedIngredients); + + _context.RecipeShoppingLists.Remove(recipeEntry); + await _context.SaveChangesAsync(); + + if (shoppingList != null) + { + await _context.Entry(shoppingList).Collection(sl => sl.RecipeShoppingList).LoadAsync(); + + if (!shoppingList.RecipeShoppingList.Any()) + { + _context.ShoppingLists.Remove(shoppingList); + await _context.SaveChangesAsync(); + } + } + } + } + + public async Task CountAllRecipeShoppinglistsAsync() + { + return await _context.RecipeShoppingLists + .CountAsync(); + } + + public async Task RemoveIngredientsFromShoppingListAsync(List recipeIngredientShoppingListIds) + { + if (recipeIngredientShoppingListIds == null || !recipeIngredientShoppingListIds.Any()) + { + throw new ArgumentNullException(nameof(recipeIngredientShoppingListIds)); + } + + using var transaction = await _context.Database.BeginTransactionAsync(); + + try + { + var affectedRecipeShoppingListIds = await _context.RecipeIngredientsShoppingLists + .Where(risl => recipeIngredientShoppingListIds.Contains(risl.Id)) + .Select(risl => risl.RecipeShoppingList.Id) + .Distinct() + .ToListAsync(); + + foreach (var id in recipeIngredientShoppingListIds) + { + var entry = await _context.RecipeIngredientsShoppingLists.FindAsync(id); + if (entry != null) + { + _context.RecipeIngredientsShoppingLists.Remove(entry); + } + } + + await _context.SaveChangesAsync(); + + foreach (var recipeShoppingListId in affectedRecipeShoppingListIds) + { + await RemoveRecipeIfEmptyAsync(recipeShoppingListId); + } + + await transaction.CommitAsync(); + } + catch + { + await transaction.RollbackAsync(); + throw; + } + } + + private IQueryable GetShoppingListQuery() + { + return _context.ShoppingLists + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.Recipe) + .ThenInclude(r => r.MediaFiles) + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.SelectedIngredients) + .ThenInclude(si => si.RecipeIngredient) + .ThenInclude(ri => ri.Ingredient) + .Include(sl => sl.RecipeShoppingList) + .ThenInclude(rsl => rsl.SelectedIngredients) + .ThenInclude(si => si.RecipeIngredient) + .ThenInclude(ri => ri.Unit); + } + } +} diff --git a/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs b/Francesco.Recipes.World/Repositories/Unit/IUnitRepository.cs new file mode 100644 index 0000000..5be0996 --- /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..29a3807 --- /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/Services/Instruction/IInstructionService.cs b/Francesco.Recipes.World/Services/Instruction/IInstructionService.cs new file mode 100644 index 0000000..27fa3a0 --- /dev/null +++ b/Francesco.Recipes.World/Services/Instruction/IInstructionService.cs @@ -0,0 +1,13 @@ +namespace Francesco.Recipes.World.Services.Instruction +{ + using Francesco.Recipes.World.Models.BackendModels.Instruction; + + public interface IInstructionService + { + Task MoveInstructionUpAsync(Guid recipeId, Guid instructionId); + + Task MoveInstructionDownAsync(Guid recipeId, Guid instructionId); + + Task> GetSortedInstructionsAsync(Guid recipeId); + } +} diff --git a/Francesco.Recipes.World/Services/Instruction/InstructionService.cs b/Francesco.Recipes.World/Services/Instruction/InstructionService.cs new file mode 100644 index 0000000..2b8fdee --- /dev/null +++ b/Francesco.Recipes.World/Services/Instruction/InstructionService.cs @@ -0,0 +1,58 @@ +namespace Francesco.Recipes.World.Services.Instruction +{ + using Francesco.Recipes.World.Models.BackendModels.Instruction; + using Francesco.Recipes.World.Repositories.Instruction; + + public class InstructionService : IInstructionService + { + private readonly IInstructionRepository _instructionRepository; + + public InstructionService(IInstructionRepository instructionRepository) + { + _instructionRepository = instructionRepository; + } + + public Task MoveInstructionUpAsync(Guid recipeId, Guid instructionId) + => MoveInstructionAsync(recipeId, instructionId, moveUp: true); + + public Task MoveInstructionDownAsync(Guid recipeId, Guid instructionId) + => MoveInstructionAsync(recipeId, instructionId, moveUp: false); + + public async Task> GetSortedInstructionsAsync(Guid recipeId) + { + var instructions = await _instructionRepository.GetInstructionsOfRecipeAsync(recipeId); + return instructions.OrderBy(i => i.Number).ToList(); + } + + private async Task MoveInstructionAsync(Guid recipeId, Guid instructionId, bool moveUp) + { + var instructions = await _instructionRepository.GetInstructionsOfRecipeAsync(recipeId); + + var instruction = instructions.FirstOrDefault(i => i.Id == instructionId); + + if (instruction == null) + { + throw new InvalidDataException($"Instruction with ID {instructionId} not found."); + } + + var minStep = 1; + var maxStep = instructions.Max(i => i.Number); + + if ((moveUp && instruction.Number == minStep) || (!moveUp && instruction.Number >= maxStep)) + { + return; + } + + // Instructions are ordered by ascending numbers (1, 2, 3, ...). + // Moving up means swapping with the instruction that has one number less (Number - 1). + // Moving down means swapping with the instruction that has one number more (Number + 1). + var targetNumber = moveUp ? instruction.Number - 1 : instruction.Number + 1; + var neighbor = instructions.FirstOrDefault(i => i.Number == targetNumber); + + if (neighbor != null) + { + await _instructionRepository.SwapInstructionNumbersAsync(instruction, neighbor); + } + } + } +} diff --git a/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs b/Francesco.Recipes.World/Views/Category/CategoryRecipesViewModel.cs new file mode 100644 index 0000000..1b2fe4c --- /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; + using Francesco.Recipes.World.Models.BackendModels.Category; + + 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/Favorite/Index.cshtml b/Francesco.Recipes.World/Views/Favorite/Index.cshtml new file mode 100644 index 0000000..b1b84af --- /dev/null +++ b/Francesco.Recipes.World/Views/Favorite/Index.cshtml @@ -0,0 +1,77 @@ +@model Francesco.Recipes.World.Models.FavoriteViewModel +@{ + ViewData["Title"] = "Favoriten"; +} + +

Favoriten

+ +
+ +
+ +@if (!Model.HasFavorites) +{ +
+ Keine Favoriten vorhanden. Füge Rezepte zu deinen Favoriten hinzu, indem du auf den Stern klickst. +
+} +else +{ +
+ @foreach (var recipe in Model.FavoriteRecipes) + { +
+
+ @{ + var mediaFile = recipe.MediaFiles?.FirstOrDefault(m => m.MimeType != null && m.MimeType.StartsWith("image/")); + var imageData = mediaFile?.Data; + var mimeType = mediaFile?.MimeType; + } + +
+ @if (imageData != null && mimeType != null) + { + @recipe.Name + } + else + { +
Kein Bild
+ } +
+ +
+
@recipe.Name
+
+ + @await Html.PartialAsync("_FavoriteButton", recipe) + + + @(recipe.PreparationTime.TotalMinutes + recipe.CookingTime.TotalMinutes)min + +
+
+ +
+
+ } +
+} diff --git a/Francesco.Recipes.World/Views/Home/Index.cshtml b/Francesco.Recipes.World/Views/Home/Index.cshtml index bcfd79a..55d12a5 100644 --- a/Francesco.Recipes.World/Views/Home/Index.cshtml +++ b/Francesco.Recipes.World/Views/Home/Index.cshtml @@ -1,8 +1,81 @@ -@{ - ViewData["Title"] = "Home Page"; +@model IEnumerable + @Html.AntiForgeryToken() + +
+ Willkommen +

Willkommen in der Rezept-App

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

@category.Category.Name

+ Alle @category.Category.Name-Rezepte anzeigen +
+ +
+ +@foreach (var recipe in category.Recipes) +{ + var imageData = recipe.ImageData; + var mimeType = recipe.MimeType; + +
+
+
+ @if (imageData != null && mimeType != null) + { + @recipe.Name + } + else + { + @recipe.Name + } +
+ +
+
@recipe.Name
+

@recipe.CookingTime

+
+ @await Html.PartialAsync("_FavoriteButton", recipe) + +
+ + + Details +
+
+
} -
-

Welcome

-

Learn about building Web apps with ASP.NET Core.

-
+ +
+ +
+
+
+ } diff --git a/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml b/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml new file mode 100644 index 0000000..e8e7a13 --- /dev/null +++ b/Francesco.Recipes.World/Views/Home/_SearchResultsPartial.cshtml @@ -0,0 +1,75 @@ +@model IEnumerable + +@{ + if (!Model.Any()) + { +

Keine Ergebnisse gefunden.

+ } + else + { +
+ @foreach (var recipe in Model) + { +
+
+
+ @if (recipe.ImageData != null && recipe.MimeType != null) + { + @recipe.Name + } + else + { + Platzhalter + } +
+ +
+
+
@recipe.Name
+

+ + @recipe.TotalTime.Hours h @recipe.TotalTime.Minutes min +

+
+ +
+
+
+ @Html.AntiForgeryToken() + + +
+
+ + Details +
+
+
+
+ } +
+ } +} diff --git a/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml b/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml new file mode 100644 index 0000000..e2ce85d --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml @@ -0,0 +1,61 @@ +@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/Create.cshtml b/Francesco.Recipes.World/Views/Recipe/Create.cshtml new file mode 100644 index 0000000..55ff934 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/Create.cshtml @@ -0,0 +1,114 @@ +@model Francesco.Recipes.World.Models.CreateRecipeViewModel +@using Francesco.Recipes.World.Models.BackendModels.Recipe +@using Francesco.Recipes.World.Models +@{ + ViewData["Title"] = "Erstelle Rezept"; +} + +

Erstelle Rezept

+ +
+
+

Allgemein

+
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ +
+
+ +
+ h +
+
+
+ +
+ min +
+
+
+
+
+ +
+
+ +
+ h +
+
+
+ +
+ min +
+
+
+
+
+
+ +
+

Zutaten

+ @await Html.PartialAsync("_IngredientsPartial", Model.IngredientViewModel ?? new IngredientViewModel +{ + RecipeId = Model.CategoryId, + Ingredients = new List(), + Units = ViewBag.Units ?? new List() +}) +
+ +
+

Anweisungen

+ @await Html.PartialAsync("_GetInstructions", Model.InstructionViewModel ?? new InstructionViewModel +{ + RecipeId = Model.CategoryId, + Instructions = new List() +}) +
+ +
+

Bild/Video

+
+ + +
+
+ + +
+
+ +
+ + Abbrechen +
+
+ + diff --git a/Francesco.Recipes.World/Views/Recipe/Details.cshtml b/Francesco.Recipes.World/Views/Recipe/Details.cshtml new file mode 100644 index 0000000..65a7001 --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/Details.cshtml @@ -0,0 +1,209 @@ +@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

+

Preparation Time: @Model.PreparationTime

+

Cooking Time: @Model.CookingTime

+
+ + @await Html.PartialAsync("_AdjustableIngredientsPartial", Model) + + @await Html.PartialAsync("_RecipeInstructionGridPartial", new Francesco.Recipes.World.Models.InstructionViewModel +{ + RecipeId = Model.Id, + Instructions = Model.Instructions.ToList() +}) + +
+ @Html.AntiForgeryToken() + +
+
+ +@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..6e868fd --- /dev/null +++ b/Francesco.Recipes.World/Views/Recipe/RemoveIngredient.cshtml @@ -0,0 +1,20 @@ +@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 +
+
+ 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/_AdjustableIngredientsPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_AdjustableIngredientsPartial.cshtml new file mode 100644 index 0000000..7855058 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_AdjustableIngredientsPartial.cshtml @@ -0,0 +1,36 @@ +@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe + +
+
+

Ingredients

+
+ +
+ + + +
+ (Original: @Model.Servings) +
+
+ +
+
    + @foreach (var ingredient in Model.RecipeIngredients) + { +
  • + + @ingredient.Ingredient.Name - + @ingredient.Quantity + @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty) + +
  • + } +
+ +
+
diff --git a/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml b/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml new file mode 100644 index 0000000..e67f795 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_FavoriteButton.cshtml @@ -0,0 +1,26 @@ +@model Francesco.Recipes.World.Models.IFavoritable + +
+ @Html.AntiForgeryToken() + + +
diff --git a/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml b/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml new file mode 100644 index 0000000..e5b718e --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml @@ -0,0 +1,39 @@ +@model Francesco.Recipes.World.Models.InstructionViewModel +@Html.AntiForgeryToken() +
+ @for (int i = 0; i < Model.Instructions.Count; i++) + { +
+
+ @if (Model.Instructions[i].MediaFiles != null && Model.Instructions[i].MediaFiles.Any()) + { + var mediaFile = Model.Instructions[i].MediaFiles.First(); + if (mediaFile.Data != null) + { +
+ Instruction Media +
+ } + } + + + + +
+
+ + +
+
+ } +
+ + + + + + + + + diff --git a/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml new file mode 100644 index 0000000..5ef25b0 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_IngredientsPartial.cshtml @@ -0,0 +1,29 @@ +@model Francesco.Recipes.World.Models.IngredientViewModel +@Html.AntiForgeryToken() +
+ @for (int i = 0; i < Model.Ingredients.Count; i++) + { +
+
+ + + + +
+
+ } +
+ + diff --git a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml index 1f862ba..c95c6c8 100644 --- a/Francesco.Recipes.World/Views/Shared/_Layout.cshtml +++ b/Francesco.Recipes.World/Views/Shared/_Layout.cshtml @@ -1,49 +1,91 @@  - - - @ViewData["Title"] - Francesco.Recipes.World - - - + + + @ViewData["Title"] - Francesco.Recipes.World + + + + -
- -
-
-
- @RenderBody() -
-
+
+ +
+
+
+ @RenderBody() +
+
-
-
- © 2024 - Francesco.Recipes.World - Privacy -
-
- - - - @await RenderSectionAsync("Scripts", required: false) + + + + + + + @await Html.PartialAsync("_ValidationScriptsPartial") + @await RenderSectionAsync("Scripts", required: false) + diff --git a/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml new file mode 100644 index 0000000..c9b7df8 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_RecipeInstructionGridPartial.cshtml @@ -0,0 +1,31 @@ +@model Francesco.Recipes.World.Models.InstructionViewModel + +
+

Instructions

+ +
+ @foreach (var instruction in Model.Instructions.OrderBy(i => i.Number)) + { +
+
+ @if (instruction.MediaFiles != null && instruction.MediaFiles.Any()) + { + var mediaFile = instruction.MediaFiles.First(); + if (mediaFile.Data != null && mediaFile.Data.Length > 0) + { + Step @instruction.Number + } + } + else + { +
+ +
+ } + @instruction.Number +
+

@instruction.Description

+
+ } +
+
\ No newline at end of file diff --git a/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml b/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml new file mode 100644 index 0000000..a816f60 --- /dev/null +++ b/Francesco.Recipes.World/Views/ShoppingList/Details.cshtml @@ -0,0 +1,106 @@ +@model Francesco.Recipes.World.Models.ShoppingListDetailsViewModel +@{ + ViewData["Title"] = "Einkaufsliste Details"; +} + +@using System.Text.Json +@{ + var recipeIngredientsJson = JsonSerializer.Serialize( + Model.RecipesInAnyShoppingList.ToDictionary( + r => r.Id, + r => r.RecipeIngredients.Select(ri => new + { + id = ri.Id, + name = ri.Ingredient.Name, + amount = ri.Quantity, + unit = ri.Unit.Name, + shoppingListId = Model.RecipeIngredientToShoppingListMap.ContainsKey(ri.Id) + ? Model.RecipeIngredientToShoppingListMap[ri.Id] + : Guid.Empty + }) + ) + ); + +} + +
+
+

Einkaufsliste Details

+
+ Anzahl Rezepte: @Model.RecipeCount +
+
+ +
+ +
+ @foreach (var recipe in Model.RecipesInAnyShoppingList) + { + var mediaFile = recipe.MediaFiles?.FirstOrDefault(); + var imageData = mediaFile?.Data; + var mimeType = mediaFile?.MimeType; + var ingredientCount = recipe.RecipeIngredients?.Count() ?? 0; + +
+
+
+ +
+
+ @if (imageData != null && mimeType != null) + { + @recipe.Name + } + else + { +
+ +
+ } +
+
+
@recipe.Name
+
+
+
+ } +
+ +
+ +
+
+
+

Zutaten

+
+
+
    +
    + +
    +
    +
    +
    +
    + + +@section Scripts { + @Html.AntiForgeryToken() + + +} diff --git a/Francesco.Recipes.World/wwwroot/css/site.css b/Francesco.Recipes.World/wwwroot/css/site.css index f8d98fc..2e90883 100644 --- a/Francesco.Recipes.World/wwwroot/css/site.css +++ b/Francesco.Recipes.World/wwwroot/css/site.css @@ -19,4 +19,218 @@ html { body { margin-bottom: 60px; -} \ No newline at end of file +} + +.instruction-item, .ingredient-item { + background-color: #f8f9fa; + padding: 15px; + margin-bottom: 10px; + border-radius: 4px; +} + +.instruction-controls, .ingredient-controls { + display: flex; + align-items: center; + gap: 10px; +} + +.instruction-media { + width: 80px; + height: 80px; + overflow: hidden; + margin-right: 10px; +} + + .instruction-media img { + width: 100%; + height: 100%; + object-fit: cover; + } + +.instruction-file { + max-width: 200px; +} + +textarea.form-control { + min-height: 80px; +} + +.btn-delete, .btn-move-up, .btn-move-down, .btn-save { + background: none; + border: none; + font-size: 1.2rem; + cursor: pointer; +} + +.btn-delete { + color: #dc3545; +} + +.btn-save { + color: #28a745; +} + +.instruction-actions { + display: flex; + justify-content: flex-end; + margin-top: 5px; +} + +.btn-add { + background-color: #007bff; + color: white; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + margin-top: 10px; +} + + +/* Recipe Instructions Grid Layout */ +.instructions-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-top: 2rem; + margin-bottom: 2rem; +} + +@media (max-width: 992px) { + .instructions-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 576px) { + .instructions-grid { + grid-template-columns: 1fr; + } +} + +.instruction-card { + border: 1px solid #ccc; + padding: 1rem; + border-radius: 8px; + background-color: white; +} + +.instruction-image { + position: relative; + width: 100%; + aspect-ratio: 1 / 1; + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + background-color: #f8f9fa; + text-align: center; +} + + .instruction-image img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 4px; + } + +.placeholder-image { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + color: #999; + background-color: #f0f0f0; + border-radius: 4px; +} + +.step-number { + position: absolute; + bottom: 8px; + right: 8px; + background: rgba(0, 0, 0, 0.6); + color: white; + padding: 3px 8px; + border-radius: 50%; + font-size: 0.875rem; + font-weight: bold; +} + +.instruction-text { + font-size: 0.9rem; + line-height: 1.4; + color: #333; +} + +.recipe-instructions-section h3 { + margin-bottom: 1.5rem; + position: relative; + padding-bottom: 0.5rem; +} + + .recipe-instructions-section h3:after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 50px; + height: 2px; + background-color: #007bff; + } + + +} + +.recipe-card { + cursor: pointer; + transition: all 0.3s ease; +} + +.active-recipe-card { + border-bottom: 4px solid #000 !important; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); +} + +.selected-ingredient .btn-outline-danger { + background-color: #dc3545 !important; + color: white !important; + border-color: #dc3545 !important; +} + +#removeSelectedButton { + display: none; + margin-top: 15px; + background-color: #FFA500; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + font-weight: bold; + transition: all 0.3s; +} + + #removeSelectedButton:hover { + background-color: #FF8C00; + } + +.ingredient-counter { + position: absolute; + top: -8px; + right: -8px; + background-color: #dc3545; + color: white; + border-radius: 50%; + width: 22px; + height: 22px; + display: flex; + justify-content: center; + align-items: center; + font-size: 0.8rem; + font-weight: bold; +} + diff --git a/Francesco.Recipes.World/wwwroot/images/star.svg b/Francesco.Recipes.World/wwwroot/images/star.svg new file mode 100644 index 0000000..25afd22 --- /dev/null +++ b/Francesco.Recipes.World/wwwroot/images/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Francesco.Recipes.World/wwwroot/js/site.js b/Francesco.Recipes.World/wwwroot/js/site.js index 0937657..f9ccb78 100644 --- a/Francesco.Recipes.World/wwwroot/js/site.js +++ b/Francesco.Recipes.World/wwwroot/js/site.js @@ -1,4 +1,416 @@ -// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification -// for details on configuring this project to bundle and minify static web assets. +(function (window, document) { + const selectedIngredients = new Set(); + const recipeIngredients = window.recipeIngredients || {}; + let activeRecipeId = Object.keys(recipeIngredients)[0]; + let recipeId; -// Write your JavaScript code. + htmx && htmx.on('htmx:afterSwap', (event) => { + if (event.target.id === 'instructions-container') { + console.log('Instructions reloaded.'); + } + if (event.target.id === 'ingredients-container') { + console.log('Ingredients reloaded.'); + } + }); + + function setRecipeId(id) { + recipeId = id; + } + + + async function updateRecipeCount() { + try { + const response = await fetch('/ShoppingList/RecipeCount'); + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); + const data = await response.json(); + document.getElementById('recipeCount').textContent = data.count; + } catch (err) { + console.error("Fehler beim Aktualisieren der Rezeptanzahl:", err); + } + } + + const countUpdateInterval = setInterval(updateRecipeCount, 5000); + + document.addEventListener('shopping-list-changed', updateRecipeCount); + + window.addEventListener('beforeunload', function () { + clearInterval(countUpdateInterval); + }); + + document.addEventListener('DOMContentLoaded', function () { + document.getElementById('carouselLeft')?.addEventListener('click', function () { + document.getElementById('recipeCarousel').scrollBy({ left: -200, behavior: 'smooth' }); + }); + document.getElementById('carouselRight')?.addEventListener('click', function () { + document.getElementById('recipeCarousel').scrollBy({ left: 200, behavior: 'smooth' }); + }); + + if (activeRecipeId) setActiveCard(activeRecipeId); + + document.querySelectorAll('.recipe-card').forEach(card => { + card.addEventListener('click', function () { + setActiveCard(this.dataset.recipeId); + }); + }); + }); + + function renderIngredientList(recipeId) { + const list = document.getElementById('ingredientList'); + list.innerHTML = ''; + const ingredients = recipeIngredients[recipeId] || []; + if (ingredients.length === 0) { + list.innerHTML = '
  • Keine Zutaten vorhanden.
  • '; + return; + } + ingredients.forEach(ingredient => { + const isSelected = selectedIngredients.has(ingredient.id); + const li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-center'; + if (isSelected) li.classList.add('selected-ingredient'); + li.innerHTML = ` + + ${ingredient.name} + ${ingredient.amount} ${ingredient.unit} + + + `; + list.appendChild(li); + }); + updateRemoveSelectedButton(); + } + + function setActiveCard(recipeId) { + document.querySelectorAll('.recipe-card').forEach(card => { + card.classList.toggle('active-recipe-card', card.dataset.recipeId === recipeId); + }); + activeRecipeId = recipeId; + renderIngredientList(recipeId); + } + + function toggleIngredientSelection(ingredientId, button) { + const listItem = button.closest('li'); + if (selectedIngredients.has(ingredientId)) { + selectedIngredients.delete(ingredientId); + listItem.classList.remove('selected-ingredient'); + } else { + selectedIngredients.add(ingredientId); + listItem.classList.add('selected-ingredient'); + } + updateRemoveSelectedButton(); + } + + function updateRemoveSelectedButton() { + document.getElementById('removeSelectedButton').style.display = + selectedIngredients.size > 0 ? 'inline-block' : 'none'; + } + + async function removeSelectedIngredients() { + if (selectedIngredients.size === 0) return; + + const shoppingListIds = []; + selectedIngredients.forEach(ingredientId => { + for (const recipeId in recipeIngredients) { + const ingredients = recipeIngredients[recipeId]; + const ingredient = ingredients.find(ing => ing.id === ingredientId); + if (ingredient && ingredient.shoppingListId && + ingredient.shoppingListId !== "00000000-0000-0000-0000-000000000000") { + shoppingListIds.push(ingredient.shoppingListId); + } + } + }); + + if (shoppingListIds.length === 0) { + console.warn("Keine gültigen Shopping List IDs gefunden"); + return; + } + + try { + const response = await fetch('/ShoppingList/RemoveIngredients', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value, + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + }, + body: JSON.stringify(shoppingListIds) + }); + + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); + + const result = await response.json(); + + if (result.success) { + window.location.reload(); + } else { + console.error("Fehler beim Entfernen der Zutaten:", result.error); + alert("Beim Entfernen der Zutaten ist ein Fehler aufgetreten."); + } + } catch (err) { + console.error("Fehler beim Entfernen der Zutaten:", err); + alert("Beim Entfernen der Zutaten ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut."); + } + } + + async function removeRecipe(recipeId) { + let confirmed = false; + + if (window.Swal) { + const result = await Swal.fire({ + title: 'Rezept entfernen?', + text: 'Möchten Sie dieses Rezept wirklich aus der Einkaufsliste entfernen?', + icon: 'warning', + showCancelButton: true, + confirmButtonText: 'Ja, entfernen', + cancelButtonText: 'Abbrechen', + confirmButtonColor: '#d33', + cancelButtonColor: '#3085d6' + }); + confirmed = result.isConfirmed; + } else { + confirmed = confirm('Möchten Sie dieses Rezept wirklich aus der Einkaufsliste entfernen?'); + } + + if (!confirmed) return; + + try { + const tokenElement = document.querySelector('input[name="__RequestVerificationToken"]'); + if (!tokenElement) { + console.error('Anti-forgery token not found'); + alert('Fehler: Anti-Forgery Token nicht gefunden'); + return; + } + + const response = await fetch(`/ShoppingList/RemoveRecipeFromList/${recipeId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'RequestVerificationToken': tokenElement.value + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const result = await response.json(); + + if (result.success) { + if (window.Swal) { + await Swal.fire({ + title: 'Erfolgreich!', + text: 'Das Rezept wurde entfernt.', + icon: 'success', + timer: 1500, + showConfirmButton: false + }); + } else { + alert('Das Rezept wurde erfolgreich entfernt.'); + } + window.location.reload(); + } else { + const errorMsg = result.error || 'Unbekannter Fehler'; + console.error('Fehler beim Entfernen:', errorMsg); + if (window.Swal) { + await Swal.fire({ + title: 'Fehler', + text: errorMsg, + icon: 'error' + }); + } else { + alert('Fehler: ' + errorMsg); + } + } + } catch (err) { + console.error('Fehler beim Entfernen des Rezepts:', err); + if (window.Swal) { + await Swal.fire({ + title: 'Fehler', + text: 'Beim Löschen ist ein Fehler aufgetreten.', + icon: 'error' + }); + } else { + alert('Beim Löschen ist ein Fehler aufgetreten.'); + } + } + } + + + async function moveInstructionUp(instructionId, recipeIdParam) { + const idToUse = recipeIdParam || recipeId; + if (!idToUse) { + alert('Recipe ID is not set. Please select a recipe first.'); + return; + } + + try { + const response = await fetch(`/Recipe/${idToUse}/Instruction/${instructionId}/move-up`, { + method: 'POST', + headers: { + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }); + + if (response.ok) { + htmx.ajax('GET', `/Recipe/${idToUse}/Instructions`, '#instructions-container'); + } else { + const error = await response.json(); + alert(error.Error || 'Failed to move instruction up.'); + } + } catch (error) { + console.error('Error moving instruction up:', error); + } + } + + async function moveInstructionDown(instructionId, recipeIdParam) { + const idToUse = recipeIdParam || recipeId; + if (!idToUse) { + alert('Recipe ID is not set. Please select a recipe first.'); + return; + } + + try { + const response = await fetch(`/Recipe/${idToUse}/Instruction/${instructionId}/move-down`, { + method: 'POST', + headers: { + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }); + + if (response.ok) { + htmx.ajax('GET', `/Recipe/${idToUse}/Instructions`, '#instructions-container'); + } else { + const error = await response.json(); + alert(error.Error || 'Failed to move instruction down.'); + } + } catch (error) { + console.error('Error moving instruction down:', error); + } + } + + async function removeInstruction(instructionId, recipeIdParam) { + const idToUse = recipeIdParam || recipeId; + if (!idToUse) { + alert('Recipe ID is not set. Please select a recipe first.'); + return; + } + if (!confirm('Möchtest du diesen Schritt wirklich löschen?')) return; + + try { + const response = await fetch(`/${idToUse}/RemoveInstruction/${instructionId}`, { + method: 'POST', + headers: { + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }); + + if (response.ok) { + const element = document.getElementById(`instruction-${instructionId}`); + if (element) element.remove(); + } else { + const error = await response.json(); + alert(error.Error || 'Fehler beim Löschen der Anweisung.'); + } + } catch (error) { + console.error('Fehler beim Löschen:', error); + } + } + + function addInstruction() { + const container = document.getElementById('instructions-container'); + const index = document.querySelectorAll('.instruction-item').length; + + const newInstructionHtml = ` +
    +
    + + + +
    +
    + + +
    +
    + `; + container.insertAdjacentHTML('beforeend', newInstructionHtml); + } + + async function removeIngredient(ingredientId) { + if (!confirm('Möchtest du diese Zutat wirklich löschen?')) return; + + try { + const response = await fetch(`/${recipeId}/RemoveIngredient/${ingredientId}`, { + method: 'POST', + headers: { + 'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value + } + }); + + if (response.ok) { + const element = document.getElementById(`ingredient-${ingredientId}`); + if (element) element.remove(); + } else { + const error = await response.json(); + alert(error.Error || 'Fehler beim Löschen der Zutat.'); + } + } catch (error) { + console.error('Fehler beim Löschen:', error); + } + } + + async function addIngredient() { + const container = document.getElementById('ingredients-container'); + const index = document.querySelectorAll('.ingredient-item').length; + + const newIngredientHtml = ` +
    +
    + + + + +
    +
    + `; + container.insertAdjacentHTML('beforeend', newIngredientHtml); + + const addedElement = document.getElementById(`ingredient-new-${index}`); + const unitSelect = addedElement.querySelector('.unit-select'); + + try { + const response = await fetch('/Unit/GetAllUnits'); + if (response.ok) { + const units = await response.json(); + unitSelect.innerHTML = ''; + units.forEach(unit => { + const option = new Option(unit.name, unit.id); + unitSelect.add(option); + }); + } else { + unitSelect.innerHTML = ''; + } + } catch (error) { + unitSelect.innerHTML = ''; + } + } + + window.Francesco = { + setRecipeId, + moveInstructionUp, + moveInstructionDown, + removeInstruction, + addInstruction, + removeIngredient, + addIngredient, + toggleIngredientSelection, + removeSelectedIngredients, + removeRecipe, + }; + +})(window, document); +console.log("Francesco-Objekt initialisiert:", window.Francesco); \ No newline at end of file diff --git a/FrancescosRecipeWorld Mock/FrancescosRecipeWorld Mock.csproj b/FrancescosRecipeWorld Mock/FrancescosRecipeWorld Mock.csproj new file mode 100644 index 0000000..f8f46d2 --- /dev/null +++ b/FrancescosRecipeWorld Mock/FrancescosRecipeWorld Mock.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + FrancescosRecipeWorld_Mock + latest + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/FrancescosRecipeWorld Mock/MSTestSettings.cs b/FrancescosRecipeWorld Mock/MSTestSettings.cs new file mode 100644 index 0000000..aaf278c --- /dev/null +++ b/FrancescosRecipeWorld Mock/MSTestSettings.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/FrancescosRecipeWorld Mock/Test1.cs b/FrancescosRecipeWorld Mock/Test1.cs new file mode 100644 index 0000000..e185433 --- /dev/null +++ b/FrancescosRecipeWorld Mock/Test1.cs @@ -0,0 +1,156 @@ +using Francesco.Recipes.World.Controller.Recipe; +using Francesco.Recipes.World.Data; +using Francesco.Recipes.World.Models; +using Francesco.Recipes.World.Models.BackendModels.Instruction; +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.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 Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Moq; + +namespace FrancescosRecipeWorld_Mock +{ + [TestClass] + public sealed class FrancescoDamicoUnittest2 + { + private Mock _mockFavoriteRepository; + private Mock _mockRecipeRepository; + private RecipeController _recipeController; + + [TestInitialize] + public void Setup() + { + _mockFavoriteRepository = new Mock(); + _mockRecipeRepository = new Mock(); + + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + _recipeController = new RecipeController( + _mockRecipeRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + _mockFavoriteRepository.Object, + new FrancescosRecipesWorldDbContext(options) + ); + } + + /// + /// Test 1: AddFavorite(Guid recipeId) -> PartialViewResult mit FavoriteButtonViewModel + /// Parameter: recipeId (Guid) + /// Rückgabewert: PartialViewResult + /// + [TestMethod] + public async Task FrancescoDamico_UnitTest1() + { + // Arrange + var recipeId = Guid.NewGuid(); + _mockFavoriteRepository + .Setup(x => x.AddFavoriteAsync(recipeId)) + .Returns(Task.CompletedTask); + + // Act + var result = await _recipeController.AddFavorite(recipeId); + + // Assert + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(PartialViewResult)); + + var partialViewResult = result as PartialViewResult; + Assert.AreEqual("_FavoriteButton", partialViewResult.ViewName); + Assert.IsInstanceOfType(partialViewResult.Model, typeof(FavoritButtonViewModel)); + + var model = partialViewResult.Model as FavoritButtonViewModel; + Assert.AreEqual(recipeId, model.Id); + Assert.IsTrue(model.IsFavorite); + } + + /// + /// Test 2: RemoveFavorite(Guid recipeId) -> PartialViewResult mit FavoriteButtonViewModel + /// Parameter: recipeId (Guid) + /// Rückgabewert: PartialViewResult + /// + [TestMethod] + public async Task FrancescoDamico_UnitTest2() + { + // Arrange + var recipeId = Guid.NewGuid(); + _mockFavoriteRepository + .Setup(x => x.RemoveFavoriteAsync(recipeId)) + .Returns(Task.CompletedTask); + + // Act + var result = await _recipeController.RemoveFavorite(recipeId); + + // Assert + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(PartialViewResult)); + + var partialViewResult = result as PartialViewResult; + Assert.AreEqual("_FavoriteButton", partialViewResult.ViewName); + Assert.IsInstanceOfType(partialViewResult.Model, typeof(FavoritButtonViewModel)); + + var model = partialViewResult.Model as FavoritButtonViewModel; + Assert.AreEqual(recipeId, model.Id); + Assert.IsFalse(model.IsFavorite); + } + + /// + /// Test 3: Details(Guid recipeId) -> ViewResult mit Recipe Modell + /// Parameter: recipeId (Guid) + /// Rückgabewert: ViewResult mit Recipe-Objekt + /// + [TestMethod] + public async Task FrancescoDamico_UnitTest3() + { + // Arrange + var recipeId = Guid.NewGuid(); + var expectedRecipe = new Recipe + { + Id = recipeId, + Name = "Spaghetti Carbonara", + Description = "Klassisches italienisches Pasta-Gericht", + Difficulty = Difficulty.Easy, + Servings = 4, + PreparationTime = new TimeSpan(0, 10, 0), + CookingTime = new TimeSpan(0, 20, 0), + IsFavorite = false, + CreatedAt = DateTime.UtcNow, + RecipeIngredients = new List(), + Instructions = new List(), + MediaFiles = new List() + }; + + _mockRecipeRepository + .Setup(x => x.GetRecipeByIdAsync(recipeId)) + .ReturnsAsync(expectedRecipe); + + // Act + var result = await _recipeController.Details(recipeId); + + // Assert + Assert.IsNotNull(result); + Assert.IsInstanceOfType(result, typeof(ViewResult)); + + var viewResult = result as ViewResult; + Assert.IsInstanceOfType(viewResult.Model, typeof(Recipe)); + + var model = viewResult.Model as Recipe; + Assert.AreEqual(recipeId, model.Id); + Assert.AreEqual("Spaghetti Carbonara", model.Name); + Assert.AreEqual(Difficulty.Easy, model.Difficulty); + } + } +}