diff --git a/Web.Api/Controllers/TaskController.cs b/Web.Api/Controllers/TaskController.cs index 43f341b..dd6b2d9 100644 --- a/Web.Api/Controllers/TaskController.cs +++ b/Web.Api/Controllers/TaskController.cs @@ -25,24 +25,29 @@ public class TaskController : ControllerBase [HttpGet("{taskId}", Name = "GetTaskById")] public async Task> GetTaskById([FromHeader] Guid userId, Guid taskId) { - if(!await _unitOfWork.User.IsUserInDbAsync(userId)) { + if (!await _unitOfWork.User.IsUserInDbAsync(userId)) + { return StatusCode(403); } - TaskItem? taskItem = await _unitOfWork.TaskItem.GetTaskByIdAsync(taskId, userId); - if (taskItem is null) { - return NotFound(taskId); + TaskItem? taskItem = await _unitOfWork.TaskItem.GetTaskByIdAsync(taskId, userId); + if (taskItem is null) + { + return NotFound(taskId); } - - TaskDto? taskDetail = new TaskDto //create a new instance of TaskDto and set their properties + TaskDto? taskDetail = new() //create a new instance of TaskDto and set their properties { Id = taskItem.Id, Title = taskItem.Title, DueDate = taskItem.DueDate, Priority = taskItem.Priority, + ParentTaskId = taskItem.SubTaskSubTaskItems.FirstOrDefault()?.TaskItemId, + + CreatedDate = taskItem.CreatedDate, CreatedUserId = taskItem.CreatedUserId, + Notes = taskItem.TaskItemNotes.Select //within the TaskDto create a new List of Notes that grabs TaskItemNotes and set their properties (note => new NoteDto //create new instance of NoteDto { @@ -68,9 +73,19 @@ public async Task> GetTaskById([FromHeader] Guid userId, G [HttpPost(Name = "CreateTask")] public async Task> CreateTask([FromHeader] Guid userId, TaskCreateDto taskCreatedDto) { - if(!await _unitOfWork.User.IsUserInDbAsync(userId)) { + if (!await _unitOfWork.User.IsUserInDbAsync(userId)) + { return StatusCode(403); - } + } + + if (taskCreatedDto.ParentTaskId.HasValue) + { + TaskItem? parentTask = await _unitOfWork.TaskItem.GetTaskByIdAsync(taskCreatedDto.ParentTaskId.Value, userId); + if (parentTask is null) + { + return NotFound(taskCreatedDto.ParentTaskId); + } + } //calls the TaskItem prop and set the task created dto to its prop //Request DTO @@ -80,15 +95,17 @@ public async Task> CreateTask([FromHeader] Guid userId, Ta { Title = taskCreatedDto.Title, Priority = taskCreatedDto.Priority, + CreatedDate = DateTime.Now, - CreatedUserId = userId, //set the UserId which is given by the user from the header + CreatedUserId = userId, TaskItemStatusHistories = [ - new TaskItemStatusHistory() { - StatusId = _statusChange.PendingId, - CreatedDate = DateTime.Now, - CreatedUserId = userId + new TaskItemStatusHistory() { + StatusId = _statusChange.PendingId, + CreatedDate = DateTime.Now, + CreatedUserId = userId } ] + }; if (taskCreatedDto.DueDate == null) @@ -96,23 +113,37 @@ public async Task> CreateTask([FromHeader] Guid userId, Ta taskCreation.DueDate = new DateTime(1900, 1, 1); //Default if null } { - taskCreation.DueDate = taskCreatedDto.DueDate.Value; //enetered value + taskCreation.DueDate = taskCreatedDto.DueDate!.Value; //enetered value + } + + //SubTask creation if ParentId is provided + if (taskCreatedDto.ParentTaskId.HasValue) + { + SubTask subTask = new() + { + TaskItemId = taskCreatedDto.ParentTaskId.Value, + SubTaskItemId = taskCreation.Id, + CreatedDate = DateTime.Now, + CreatedUserId = userId + }; + taskCreation.SubTaskSubTaskItems.Add(subTask); } await _unitOfWork.TaskItem.CreateTaskAsync(taskCreation); //UofW takes the TaskItem class and calls the CreateTask method from the TaskItemRepo await _unitOfWork.SaveChangesAsync(); //UofW calls the SaveChanges method - taskCreation = await _unitOfWork.TaskItem.GetTaskByIdAsync(taskCreation.Id, userId); + taskCreation = await _unitOfWork.TaskItem.GetTaskByIdAsync(taskCreation.Id, userId) ?? throw new Exception("The object you tried to create does not exist in db"); //Response DTO //create a new instance of TaskDto //calls the TaskDto prop and call the taskCreation and set the prop for user view //return the result of the tasks created - TaskDto creationResult = new TaskDto() + TaskDto creationResult = new() { Id = taskCreation.Id, Title = taskCreation.Title, DueDate = taskCreation.DueDate, Priority = taskCreation.Priority, + ParentTaskId = taskCreation.SubTaskSubTaskItems.FirstOrDefault()?.TaskItemId, Notes = taskCreation.TaskItemNotes.Select (note => new NoteDto @@ -143,15 +174,18 @@ public async Task> CreateTask([FromHeader] Guid userId, Ta public async Task> CreateNote([FromHeader] Guid userId, Guid taskId, NoteCreateDto noteCreateDto) { - if(!await _unitOfWork.User.IsUserInDbAsync(userId)) { + if (!await _unitOfWork.User.IsUserInDbAsync(userId)) + { return StatusCode(403); } - TaskItem? taskItem = await _unitOfWork.TaskItem.GetTaskByIdAsync(taskId, userId); - if (taskItem is null) { - return NotFound(taskId); + TaskItem? taskItem = await _unitOfWork.TaskItem.GetTaskByIdAsync(taskId, userId); + if (taskItem is null) + { + return NotFound(taskId); } + //Request DTO TaskItemNote noteCreation = new TaskItemNote { TaskItemId = taskId, @@ -163,6 +197,7 @@ public async Task> CreateNote([FromHeader] Guid user await _unitOfWork.TaskItem.CreateNoteAsync(noteCreation); await _unitOfWork.SaveChangesAsync(); + //Response DTO var noteResult = new NoteDto { Id = noteCreation.Id, @@ -186,17 +221,19 @@ public Task>> GetAllNotes([FromHeader] Guid userId, G public async Task> DeleteNote([FromHeader] Guid userId, Guid taskId, Guid noteId) { - if(!await _unitOfWork.User.IsUserInDbAsync(userId)) { + if (!await _unitOfWork.User.IsUserInDbAsync(userId)) + { return StatusCode(403); } - TaskItem? taskItem = await _unitOfWork.TaskItem.GetTaskByIdAsync(taskId, userId); - if (taskItem is null) { - return NotFound(taskId); + TaskItem? taskItem = await _unitOfWork.TaskItem.GetTaskByIdAsync(taskId, userId); + if (taskItem is null) + { + return NotFound(taskId); } TaskItemNote? note = taskItem.TaskItemNotes.SingleOrDefault(n => n.Id == noteId); - if(note is null) + if (note is null) { return NotFound(noteId); } @@ -204,6 +241,7 @@ public async Task> DeleteNote([FromHeader] Guid userId, Gu _unitOfWork.TaskItem.DeleteNote(note); await _unitOfWork.SaveChangesAsync(); + //Response DTO NoteDto deleteNote = new NoteDto { Id = note.Id, @@ -215,11 +253,48 @@ public async Task> DeleteNote([FromHeader] Guid userId, Gu return Ok(deleteNote); } + private bool HasIncompletedDescendants(TaskItem task) + { + //If task has no subtasks end the check and return false + if (task.SubTaskTaskItems == null) + { + return false; + } + + //Loop through every single subtask + foreach (var sub in task.SubTaskTaskItems) + { + //Get the child task from the subtask + TaskItem child = sub.TaskItem; + + //Get the latest status history of a subtask and sort by created date + TaskItemStatusHistory? latestStatus = child.TaskItemStatusHistories + .OrderByDescending(c => c.CreatedDate) + .FirstOrDefault(); + //Check if the latest status is not complete (StatusId != CompleteId) + bool inCompletedChild =latestStatus!.StatusId != _statusChange.CompleteId; + + //If task is incomplete return true + if (inCompletedChild) + { + return true; + } + + //Recursiveley check for any SubTask that is incomplete + if (HasIncompletedDescendants(child)) + { + return true; + } + } + //If all subtasks in descedndants are complete end the check + return false; + } [HttpPost("{taskId}/status-change/complete", Name = "StatusChangeComplete")] public async Task> StatusChangeComplete([FromHeader] Guid userId, Guid taskId) { - if(!await _unitOfWork.User.IsUserInDbAsync(userId)) { + if (!await _unitOfWork.User.IsUserInDbAsync(userId)) + { return StatusCode(403); } @@ -229,8 +304,15 @@ public async Task> StatusChangeComplete([FromHeader] Guid return NotFound(taskId); } + // Prevent completing a task when any child SubTask is not complete. + if (HasIncompletedDescendants(taskItem)) + { + return BadRequest("Cannot complete parent task with incomplete child sub-tasks."); + } - TaskItemStatusHistory newTaskStatus = new TaskItemStatusHistory + //add new status history for Complete + //Reuest DTO + TaskItemStatusHistory newTaskStatus = new() { TaskItemId = taskItem.Id, StatusId = _statusChange.CompleteId, @@ -238,16 +320,17 @@ public async Task> StatusChangeComplete([FromHeader] Guid CreatedUserId = userId, }; - taskItem.TaskItemStatusHistories.Add(newTaskStatus); await _unitOfWork.SaveChangesAsync(); - TaskDto statusResult = new TaskDto + //Response DTO + TaskDto statusResult = new() { Id = taskItem.Id, Title = taskItem.Title, DueDate = taskItem.DueDate, Priority = taskItem.Priority, + ParentTaskId = taskItem.SubTaskSubTaskItems.FirstOrDefault()?.TaskItemId, Notes = taskItem.TaskItemNotes.Select(n => new NoteDto { @@ -260,7 +343,7 @@ public async Task> StatusChangeComplete([FromHeader] Guid CurrentStatus = new StatusDto { - Id = taskItem.Id, + Id = _statusChange.CompleteId, Name = _statusChange.Complete, Code = _statusChange.Code2 }, @@ -282,7 +365,8 @@ public async Task> StatusChangePending([FromHeader] Guid u [HttpPut("{taskId}", Name = "EditTask")] public async Task> EditTask([FromHeader] Guid userId, Guid taskId, TaskDto updateTaskDto) { - if(!await _unitOfWork.User.IsUserInDbAsync(userId)) { + if (!await _unitOfWork.User.IsUserInDbAsync(userId)) + { return StatusCode(403); } @@ -292,6 +376,31 @@ public async Task> EditTask([FromHeader] Guid userId, Guid return NotFound(taskId); } + //check if parent task exists + if (updateTaskDto.ParentTaskId.HasValue) + { + TaskItem? parentTask = await _unitOfWork.TaskItem.GetTaskByIdAsync(updateTaskDto.ParentTaskId.Value, userId); + if (parentTask is null) + { + return NotFound(updateTaskDto.ParentTaskId); + } + } + + //subtask creation if ParentId is provided + if (updateTaskDto.ParentTaskId.HasValue) + { + SubTask? subTask = new() + { + TaskItemId = updateTaskDto.ParentTaskId.Value, + SubTaskItemId = taskItem.Id, + CreatedDate = DateTime.Now, + CreatedUserId = userId + }; + taskItem.SubTaskSubTaskItems.Add(subTask); + } + await _unitOfWork.SaveChangesAsync(); + + //update only provided property fields if (updateTaskDto.Title != null && updateTaskDto.DueDate.HasValue && updateTaskDto.Priority != 0) @@ -300,15 +409,16 @@ public async Task> EditTask([FromHeader] Guid userId, Guid taskItem.DueDate = updateTaskDto.DueDate.Value; taskItem.Priority = updateTaskDto.Priority; } - await _unitOfWork.SaveChangesAsync(); + //Response DTO TaskDto editTaskResult = new TaskDto { Id = taskItem.Id, Title = taskItem.Title, DueDate = taskItem.DueDate, Priority = taskItem.Priority, + ParentTaskId = taskItem.SubTaskSubTaskItems.FirstOrDefault()?.TaskItemId, Notes = taskItem.TaskItemNotes.Select(n => new NoteDto { @@ -332,5 +442,6 @@ public async Task> EditTask([FromHeader] Guid userId, Guid }; return Ok(editTaskResult); } + } } diff --git a/Web.Api/Dto/Request/TaskCreateDto.cs b/Web.Api/Dto/Request/TaskCreateDto.cs index 14ddf46..7bbe923 100644 --- a/Web.Api/Dto/Request/TaskCreateDto.cs +++ b/Web.Api/Dto/Request/TaskCreateDto.cs @@ -5,5 +5,7 @@ public class TaskCreateDto public string Title { get; set; } public DateTime? DueDate { get; set; } public int Priority { get; set; } + public Guid? ParentTaskId { get; set; } + } } diff --git a/Web.Api/Dto/Response/TaskDto.cs b/Web.Api/Dto/Response/TaskDto.cs index 62561a3..c3087f4 100644 --- a/Web.Api/Dto/Response/TaskDto.cs +++ b/Web.Api/Dto/Response/TaskDto.cs @@ -1,4 +1,6 @@ -namespace Web.Api.Dto.Response +using Web.Api.Persistence.Models; + +namespace Web.Api.Dto.Response { public class TaskDto { @@ -6,6 +8,7 @@ public class TaskDto public string Title { get; set; } public DateTime? DueDate { get; set; } public int Priority { get; set; } + public Guid? ParentTaskId { get; set; } public List Notes { get; set; } = []; public StatusDto? CurrentStatus { get; set; } public DateTime CreatedDate { get; set; } diff --git a/Web.Api/Persistence/Models/TaskItem.cs b/Web.Api/Persistence/Models/TaskItem.cs index a8c59e2..f3e2330 100644 --- a/Web.Api/Persistence/Models/TaskItem.cs +++ b/Web.Api/Persistence/Models/TaskItem.cs @@ -10,9 +10,7 @@ public partial class TaskItem public string Title { get; set; } = null!; public DateTime? DueDate { get; set; } - public int Priority { get; set; } - public DateTime CreatedDate { get; set; } public Guid CreatedUserId { get; set; } diff --git a/Web.Api/Persistence/Repositories/TaskItemRepo.cs b/Web.Api/Persistence/Repositories/TaskItemRepo.cs index 38199e7..97bf012 100644 --- a/Web.Api/Persistence/Repositories/TaskItemRepo.cs +++ b/Web.Api/Persistence/Repositories/TaskItemRepo.cs @@ -9,14 +9,11 @@ namespace Web.Api.Persistence.Repositories public class TaskItemRepo { private readonly TaskManagerAppDBContext _context; - - public TaskItemRepo(TaskManagerAppDBContext context) { _context = context; } - /// /// Get Task by Id that only pertains to specific user. If not found, returns null. /// @@ -25,10 +22,20 @@ public TaskItemRepo(TaskManagerAppDBContext context) /// public async Task GetTaskByIdAsync(Guid taskId, Guid userId) { - return await _context.TaskItems.Include(item => item.TaskItemNotes).Include(history => history.TaskItemStatusHistories) - .ThenInclude(stat => stat.Status).SingleOrDefaultAsync(ti => ti.Id == taskId && ti.CreatedUserId == userId); + return await _context.TaskItems + .Include(item => item.TaskItemNotes) + .Include(item => item.TaskItemStatusHistories) + .ThenInclude(stat => stat.Status) + .Include(t => t.SubTaskSubTaskItems) + .ThenInclude(st => st.TaskItem) + .Include(t => t.SubTaskTaskItems) + .ThenInclude(st => st.SubTaskItem) + .ThenInclude(si => si.TaskItemStatusHistories) + .ThenInclude(h => h.Status) + .SingleOrDefaultAsync(ti => ti.Id == taskId && ti.CreatedUserId == userId); } + public async Task CreateTaskAsync(TaskItem taskItem) { @@ -36,21 +43,17 @@ public async Task CreateTaskAsync(TaskItem taskItem) } - public async Task CreateNoteAsync(TaskItemNote taskItemItemNote) { await _context.AddAsync(taskItemItemNote); } - public IEnumerable GetAllNotes(Guid taskId) { throw new NotImplementedException(); } - - public void DeleteNote(TaskItemNote taskItemNote) { _context.Remove(taskItemNote);