diff --git a/Francesco.Recipes.World/Controller/Instruction/InstructionController.cs b/Francesco.Recipes.World/Controller/Instruction/InstructionController.cs index d86aadf..a770783 100644 --- a/Francesco.Recipes.World/Controller/Instruction/InstructionController.cs +++ b/Francesco.Recipes.World/Controller/Instruction/InstructionController.cs @@ -1,6 +1,69 @@ namespace Francesco.Recipes.World.Controller.Instruction { - public class InstructionController + 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 index 0d9ddf3..1f17d7a 100644 --- a/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs +++ b/Francesco.Recipes.World/Controller/MediaFile/MediaFileController.cs @@ -1,6 +1,5 @@ namespace Francesco.Recipes.World.Controller.MediaFile { - using Francesco.Recipes.World.Data; using Francesco.Recipes.World.Repositories.MediaFile; using Microsoft.AspNetCore.Mvc; @@ -8,12 +7,10 @@ public class MediaFileController : Controller { private readonly IMediaFileRepository _mediaFileRepository; - private readonly FrancescosRecipesWorldDbContext _context; - public MediaFileController(IMediaFileRepository mediaFileRepository, FrancescosRecipesWorldDbContext context) + public MediaFileController(IMediaFileRepository mediaFileRepository) { _mediaFileRepository = mediaFileRepository; - _context = context; } // POST: /UploadImage diff --git a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs index 24d9618..72bf903 100644 --- a/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs +++ b/Francesco.Recipes.World/Controller/Recipe/RecipeController.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; - public class RecipeController : Controller { private readonly IRecipeRepository _recipeRepository; @@ -189,6 +188,47 @@ 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(); + } + + // POST: /Recipe/{recipeId}/RemoveInstruction/{instructionId} + [HttpPost("{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) @@ -219,11 +259,11 @@ // POST: /Recipe/{recipeId}/AddInstruction [HttpPost("{recipeId}/AddInstruction")] [ValidateAntiForgeryToken] - public async Task AddInstruction(Guid recipeId, string description, int number) + public async Task AddInstruction(Guid recipeId, string description) { try { - await _instructionRepository.CreateInstructionToRecipeAsync(recipeId, description, number); + await _instructionRepository.CreateInstructionToRecipeAsync(recipeId, description); return RedirectToAction("AddInstruction", new { recipeId }); } catch (Exception ex) diff --git a/Francesco.Recipes.World/Models/InstructionViewModel.cs b/Francesco.Recipes.World/Models/InstructionViewModel.cs new file mode 100644 index 0000000..28c6911 --- /dev/null +++ b/Francesco.Recipes.World/Models/InstructionViewModel.cs @@ -0,0 +1,11 @@ +namespace Francesco.Recipes.World.Models +{ + using Francesco.Recipes.World.Models.BackendModels.Instruction; + + public class InstructionViewModel + { + public Guid RecipeId { get; set; } + + public List Instructions { get; set; } = new List(); + } +} diff --git a/Francesco.Recipes.World/Program.cs b/Francesco.Recipes.World/Program.cs index 40317b6..1caf8cf 100644 --- a/Francesco.Recipes.World/Program.cs +++ b/Francesco.Recipes.World/Program.cs @@ -7,6 +7,7 @@ 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; var builder = WebApplication.CreateBuilder(args); @@ -40,6 +41,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs index 963099b..ef8094f 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/IInstructionRepository.cs @@ -6,10 +6,14 @@ { Task GetInstructionAsync(Guid instructionId); - Task CreateInstructionToRecipeAsync(Guid recipeId, string description, int number); - - Task> GetInstructionsByRecipeIdAsync(Guid recipeId); + Task CreateInstructionToRecipeAsync(Guid recipeId, string description); 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 index b487d57..6844c0a 100644 --- a/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs +++ b/Francesco.Recipes.World/Repositories/Instruction/InstructionRepository.cs @@ -22,25 +22,30 @@ return instruction ?? throw new InvalidDataException($"Instruction {instructionId} not found."); } - public async Task CreateInstructionToRecipeAsync(Guid recipeId, string description, int number) + public async Task CreateInstructionToRecipeAsync(Guid recipeId, string description) { - var recipe = await _recipeRepository.GetRecipeAsync(recipeId); - if (string.IsNullOrWhiteSpace(description)) { throw new ArgumentException("Description cannot be empty", nameof(description)); } - if (number <= 0) + var recipe = await _context.Recipes + .Include(r => r.Instructions) + .FirstOrDefaultAsync(r => r.Id == recipeId); + + if (recipe == null) { - throw new ArgumentOutOfRangeException(nameof(number), "Number must be greater than 0."); + throw new ArgumentException("Recipe not found.", nameof(recipeId)); } + var nextNumber = recipe.Instructions?.Max(i => i.Number) ?? 0; + nextNumber++; + var newInstruction = new Instruction { Id = Guid.NewGuid(), Description = description, - Number = number, + Number = nextNumber, Recipe = recipe, }; @@ -65,17 +70,68 @@ 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> GetInstructionsByRecipeIdAsync(Guid recipeId) + public async Task> GetInstructionsOfRecipeAsync(Guid recipeId) { - return await _context.Instructions - .Include(i => i.Recipe) + var instructions = await _context.Instructions .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/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/Recipe/AddInstruction.cshtml b/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml index d6d5f79..e2ce85d 100644 --- a/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml +++ b/Francesco.Recipes.World/Views/Recipe/AddInstruction.cshtml @@ -24,36 +24,38 @@ -
- - -
- + + @section Scripts { - if (response.ok) { - location.reload(); - } else { - alert('Failed to add instruction.'); - } - } - } diff --git a/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml b/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml new file mode 100644 index 0000000..0c8a725 --- /dev/null +++ b/Francesco.Recipes.World/Views/Shared/_GetInstructions.cshtml @@ -0,0 +1,118 @@ +@model Francesco.Recipes.World.Models.InstructionViewModel +@Html.AntiForgeryToken() +
+ @for (int i = 0; i < Model.Instructions.Count; i++) + { +
+
+ + + +
+
+ + +
+
+ } +
+ + + + +@section Scripts { + +}