Allow setting ParentTaskId when updating a task via the API (with validation that the parent exists) and when creating a task via the MCP create_task tool. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
196 lines
6.7 KiB
C#
196 lines
6.7 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using TaskTracker.Core.DTOs;
|
|
using TaskTracker.Core.Entities;
|
|
using TaskTracker.Core.Enums;
|
|
using TaskTracker.Core.Interfaces;
|
|
|
|
namespace TaskTracker.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class TasksController(ITaskRepository taskRepo, ILogger<TasksController> logger) : ControllerBase
|
|
{
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetAll([FromQuery] WorkTaskStatus? status, [FromQuery] int? parentId, [FromQuery] bool includeSubTasks = false)
|
|
{
|
|
var tasks = await taskRepo.GetAllAsync(status, parentId, includeSubTasks);
|
|
return Ok(ApiResponse<List<WorkTask>>.Ok(tasks));
|
|
}
|
|
|
|
[HttpGet("active")]
|
|
public async Task<IActionResult> GetActive()
|
|
{
|
|
var task = await taskRepo.GetActiveTaskAsync();
|
|
if (task is null)
|
|
return Ok(ApiResponse<WorkTask?>.Ok(null));
|
|
return Ok(ApiResponse<WorkTask>.Ok(task));
|
|
}
|
|
|
|
[HttpGet("{id:int}")]
|
|
public async Task<IActionResult> GetById(int id)
|
|
{
|
|
var task = await taskRepo.GetByIdAsync(id);
|
|
if (task is null)
|
|
return NotFound(ApiResponse.Fail("Task not found"));
|
|
return Ok(ApiResponse<WorkTask>.Ok(task));
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> Create([FromBody] CreateTaskRequest request)
|
|
{
|
|
if (request.ParentTaskId.HasValue)
|
|
{
|
|
var parent = await taskRepo.GetByIdAsync(request.ParentTaskId.Value);
|
|
if (parent is null)
|
|
return BadRequest(ApiResponse.Fail("Parent task not found"));
|
|
}
|
|
|
|
var task = new WorkTask
|
|
{
|
|
Title = request.Title,
|
|
Description = request.Description,
|
|
Category = request.Category,
|
|
ParentTaskId = request.ParentTaskId,
|
|
EstimatedMinutes = request.EstimatedMinutes,
|
|
Status = WorkTaskStatus.Pending
|
|
};
|
|
|
|
var created = await taskRepo.CreateAsync(task);
|
|
logger.LogInformation("Created task {TaskId}: {Title}", created.Id, created.Title);
|
|
return CreatedAtAction(nameof(GetById), new { id = created.Id }, ApiResponse<WorkTask>.Ok(created));
|
|
}
|
|
|
|
[HttpPut("{id:int}/start")]
|
|
public async Task<IActionResult> Start(int id)
|
|
{
|
|
var task = await taskRepo.GetByIdAsync(id);
|
|
if (task is null)
|
|
return NotFound(ApiResponse.Fail("Task not found"));
|
|
|
|
// Pause any currently active task
|
|
var active = await taskRepo.GetActiveTaskAsync();
|
|
if (active is not null && active.Id != id)
|
|
{
|
|
active.Status = WorkTaskStatus.Paused;
|
|
await taskRepo.UpdateAsync(active);
|
|
}
|
|
|
|
task.Status = WorkTaskStatus.Active;
|
|
task.StartedAt ??= DateTime.UtcNow;
|
|
await taskRepo.UpdateAsync(task);
|
|
|
|
logger.LogInformation("Started task {TaskId}", id);
|
|
return Ok(ApiResponse<WorkTask>.Ok(task));
|
|
}
|
|
|
|
[HttpPut("{id:int}/pause")]
|
|
public async Task<IActionResult> Pause(int id, [FromBody] TaskActionRequest? request)
|
|
{
|
|
var task = await taskRepo.GetByIdAsync(id);
|
|
if (task is null)
|
|
return NotFound(ApiResponse.Fail("Task not found"));
|
|
|
|
task.Status = WorkTaskStatus.Paused;
|
|
|
|
if (!string.IsNullOrWhiteSpace(request?.Note))
|
|
{
|
|
task.Notes.Add(new TaskNote
|
|
{
|
|
Content = request.Note,
|
|
Type = NoteType.PauseNote,
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
}
|
|
|
|
await taskRepo.UpdateAsync(task);
|
|
logger.LogInformation("Paused task {TaskId}", id);
|
|
return Ok(ApiResponse<WorkTask>.Ok(task));
|
|
}
|
|
|
|
[HttpPut("{id:int}/resume")]
|
|
public async Task<IActionResult> Resume(int id, [FromBody] TaskActionRequest? request)
|
|
{
|
|
var task = await taskRepo.GetByIdAsync(id);
|
|
if (task is null)
|
|
return NotFound(ApiResponse.Fail("Task not found"));
|
|
|
|
// Pause any currently active task
|
|
var active = await taskRepo.GetActiveTaskAsync();
|
|
if (active is not null && active.Id != id)
|
|
{
|
|
active.Status = WorkTaskStatus.Paused;
|
|
await taskRepo.UpdateAsync(active);
|
|
}
|
|
|
|
task.Status = WorkTaskStatus.Active;
|
|
|
|
if (!string.IsNullOrWhiteSpace(request?.Note))
|
|
{
|
|
task.Notes.Add(new TaskNote
|
|
{
|
|
Content = request.Note,
|
|
Type = NoteType.ResumeNote,
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
}
|
|
|
|
await taskRepo.UpdateAsync(task);
|
|
logger.LogInformation("Resumed task {TaskId}", id);
|
|
return Ok(ApiResponse<WorkTask>.Ok(task));
|
|
}
|
|
|
|
[HttpPut("{id:int}/complete")]
|
|
public async Task<IActionResult> Complete(int id)
|
|
{
|
|
var task = await taskRepo.GetByIdAsync(id);
|
|
if (task is null)
|
|
return NotFound(ApiResponse.Fail("Task not found"));
|
|
|
|
// Block completion if any subtasks are not completed/abandoned
|
|
var incompleteSubTasks = task.SubTasks
|
|
.Where(st => st.Status != WorkTaskStatus.Completed && st.Status != WorkTaskStatus.Abandoned)
|
|
.ToList();
|
|
|
|
if (incompleteSubTasks.Count > 0)
|
|
return BadRequest(ApiResponse.Fail($"Cannot complete task: {incompleteSubTasks.Count} subtask(s) are still incomplete"));
|
|
|
|
task.Status = WorkTaskStatus.Completed;
|
|
task.CompletedAt = DateTime.UtcNow;
|
|
await taskRepo.UpdateAsync(task);
|
|
|
|
logger.LogInformation("Completed task {TaskId}", id);
|
|
return Ok(ApiResponse<WorkTask>.Ok(task));
|
|
}
|
|
|
|
[HttpPut("{id:int}")]
|
|
public async Task<IActionResult> Update(int id, [FromBody] UpdateTaskRequest request)
|
|
{
|
|
var task = await taskRepo.GetByIdAsync(id);
|
|
if (task is null)
|
|
return NotFound(ApiResponse.Fail("Task not found"));
|
|
|
|
if (request.Title is not null) task.Title = request.Title;
|
|
if (request.Description is not null) task.Description = request.Description;
|
|
if (request.Category is not null) task.Category = request.Category;
|
|
if (request.EstimatedMinutes.HasValue) task.EstimatedMinutes = request.EstimatedMinutes;
|
|
if (request.ParentTaskId.HasValue)
|
|
{
|
|
var parent = await taskRepo.GetByIdAsync(request.ParentTaskId.Value);
|
|
if (parent is null)
|
|
return BadRequest(ApiResponse.Fail("Parent task not found"));
|
|
task.ParentTaskId = request.ParentTaskId;
|
|
}
|
|
|
|
await taskRepo.UpdateAsync(task);
|
|
return Ok(ApiResponse<WorkTask>.Ok(task));
|
|
}
|
|
|
|
[HttpDelete("{id:int}")]
|
|
public async Task<IActionResult> Delete(int id)
|
|
{
|
|
await taskRepo.DeleteAsync(id);
|
|
logger.LogInformation("Abandoned task {TaskId}", id);
|
|
return Ok(ApiResponse.Ok());
|
|
}
|
|
}
|