In progress
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled

This commit is contained in:
Yaojia Wang
2025-11-03 11:51:02 +01:00
parent 24fb646739
commit fe8ad1c1f9
101 changed files with 26471 additions and 250 deletions

View File

@@ -0,0 +1,190 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateStory;
using ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateStory;
using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteStory;
using ColaFlow.Modules.ProjectManagement.Application.Commands.AssignStory;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoryById;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoriesByEpicId;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetStoriesByProjectId;
namespace ColaFlow.API.Controllers;
/// <summary>
/// Stories API Controller
/// </summary>
[ApiController]
[Route("api/v1")]
public class StoriesController : ControllerBase
{
private readonly IMediator _mediator;
public StoriesController(IMediator mediator)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
/// <summary>
/// Get story by ID
/// </summary>
[HttpGet("stories/{id:guid}")]
[ProducesResponseType(typeof(StoryDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetStory(Guid id, CancellationToken cancellationToken = default)
{
var query = new GetStoryByIdQuery(id);
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Get all stories for an epic
/// </summary>
[HttpGet("epics/{epicId:guid}/stories")]
[ProducesResponseType(typeof(List<StoryDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetEpicStories(Guid epicId, CancellationToken cancellationToken = default)
{
var query = new GetStoriesByEpicIdQuery(epicId);
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Get all stories for a project
/// </summary>
[HttpGet("projects/{projectId:guid}/stories")]
[ProducesResponseType(typeof(List<StoryDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetProjectStories(Guid projectId, CancellationToken cancellationToken = default)
{
var query = new GetStoriesByProjectIdQuery(projectId);
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Create a new story
/// </summary>
[HttpPost("epics/{epicId:guid}/stories")]
[ProducesResponseType(typeof(StoryDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> CreateStory(
Guid epicId,
[FromBody] CreateStoryRequest request,
CancellationToken cancellationToken = default)
{
var command = new CreateStoryCommand
{
EpicId = epicId,
Title = request.Title,
Description = request.Description,
Priority = request.Priority,
AssigneeId = request.AssigneeId,
EstimatedHours = request.EstimatedHours,
CreatedBy = request.CreatedBy
};
var result = await _mediator.Send(command, cancellationToken);
return CreatedAtAction(nameof(GetStory), new { id = result.Id }, result);
}
/// <summary>
/// Update an existing story
/// </summary>
[HttpPut("stories/{id:guid}")]
[ProducesResponseType(typeof(StoryDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateStory(
Guid id,
[FromBody] UpdateStoryRequest request,
CancellationToken cancellationToken = default)
{
var command = new UpdateStoryCommand
{
StoryId = id,
Title = request.Title,
Description = request.Description,
Status = request.Status,
Priority = request.Priority,
AssigneeId = request.AssigneeId,
EstimatedHours = request.EstimatedHours
};
var result = await _mediator.Send(command, cancellationToken);
return Ok(result);
}
/// <summary>
/// Delete a story
/// </summary>
[HttpDelete("stories/{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> DeleteStory(Guid id, CancellationToken cancellationToken = default)
{
var command = new DeleteStoryCommand { StoryId = id };
await _mediator.Send(command, cancellationToken);
return NoContent();
}
/// <summary>
/// Assign a story to a user
/// </summary>
[HttpPut("stories/{id:guid}/assign")]
[ProducesResponseType(typeof(StoryDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> AssignStory(
Guid id,
[FromBody] AssignStoryRequest request,
CancellationToken cancellationToken = default)
{
var command = new AssignStoryCommand
{
StoryId = id,
AssigneeId = request.AssigneeId
};
var result = await _mediator.Send(command, cancellationToken);
return Ok(result);
}
}
/// <summary>
/// Request model for creating a story
/// </summary>
public record CreateStoryRequest
{
public string Title { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public string Priority { get; init; } = "Medium";
public Guid? AssigneeId { get; init; }
public decimal? EstimatedHours { get; init; }
public Guid CreatedBy { get; init; }
}
/// <summary>
/// Request model for updating a story
/// </summary>
public record UpdateStoryRequest
{
public string Title { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public string? Status { get; init; }
public string? Priority { get; init; }
public Guid? AssigneeId { get; init; }
public decimal? EstimatedHours { get; init; }
}
/// <summary>
/// Request model for assigning a story
/// </summary>
public record AssignStoryRequest
{
public Guid AssigneeId { get; init; }
}

View File

@@ -0,0 +1,230 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateTask;
using ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateTask;
using ColaFlow.Modules.ProjectManagement.Application.Commands.DeleteTask;
using ColaFlow.Modules.ProjectManagement.Application.Commands.AssignTask;
using ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateTaskStatus;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetTaskById;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetTasksByStoryId;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetTasksByProjectId;
namespace ColaFlow.API.Controllers;
/// <summary>
/// Tasks API Controller
/// </summary>
[ApiController]
[Route("api/v1")]
public class TasksController : ControllerBase
{
private readonly IMediator _mediator;
public TasksController(IMediator mediator)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
/// <summary>
/// Get task by ID
/// </summary>
[HttpGet("tasks/{id:guid}")]
[ProducesResponseType(typeof(TaskDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetTask(Guid id, CancellationToken cancellationToken = default)
{
var query = new GetTaskByIdQuery(id);
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Get all tasks for a story
/// </summary>
[HttpGet("stories/{storyId:guid}/tasks")]
[ProducesResponseType(typeof(List<TaskDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetStoryTasks(Guid storyId, CancellationToken cancellationToken = default)
{
var query = new GetTasksByStoryIdQuery(storyId);
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Get all tasks for a project (for Kanban board)
/// </summary>
[HttpGet("projects/{projectId:guid}/tasks")]
[ProducesResponseType(typeof(List<TaskDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetProjectTasks(
Guid projectId,
[FromQuery] string? status = null,
[FromQuery] Guid? assigneeId = null,
CancellationToken cancellationToken = default)
{
var query = new GetTasksByProjectIdQuery
{
ProjectId = projectId,
Status = status,
AssigneeId = assigneeId
};
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Create a new task
/// </summary>
[HttpPost("stories/{storyId:guid}/tasks")]
[ProducesResponseType(typeof(TaskDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> CreateTask(
Guid storyId,
[FromBody] CreateTaskRequest request,
CancellationToken cancellationToken = default)
{
var command = new CreateTaskCommand
{
StoryId = storyId,
Title = request.Title,
Description = request.Description,
Priority = request.Priority,
EstimatedHours = request.EstimatedHours,
AssigneeId = request.AssigneeId,
CreatedBy = request.CreatedBy
};
var result = await _mediator.Send(command, cancellationToken);
return CreatedAtAction(nameof(GetTask), new { id = result.Id }, result);
}
/// <summary>
/// Update an existing task
/// </summary>
[HttpPut("tasks/{id:guid}")]
[ProducesResponseType(typeof(TaskDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateTask(
Guid id,
[FromBody] UpdateTaskRequest request,
CancellationToken cancellationToken = default)
{
var command = new UpdateTaskCommand
{
TaskId = id,
Title = request.Title,
Description = request.Description,
Status = request.Status,
Priority = request.Priority,
EstimatedHours = request.EstimatedHours,
AssigneeId = request.AssigneeId
};
var result = await _mediator.Send(command, cancellationToken);
return Ok(result);
}
/// <summary>
/// Delete a task
/// </summary>
[HttpDelete("tasks/{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> DeleteTask(Guid id, CancellationToken cancellationToken = default)
{
var command = new DeleteTaskCommand { TaskId = id };
await _mediator.Send(command, cancellationToken);
return NoContent();
}
/// <summary>
/// Assign a task to a user
/// </summary>
[HttpPut("tasks/{id:guid}/assign")]
[ProducesResponseType(typeof(TaskDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> AssignTask(
Guid id,
[FromBody] AssignTaskRequest request,
CancellationToken cancellationToken = default)
{
var command = new AssignTaskCommand
{
TaskId = id,
AssigneeId = request.AssigneeId
};
var result = await _mediator.Send(command, cancellationToken);
return Ok(result);
}
/// <summary>
/// Update task status (for Kanban board drag & drop)
/// </summary>
[HttpPut("tasks/{id:guid}/status")]
[ProducesResponseType(typeof(TaskDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateTaskStatus(
Guid id,
[FromBody] UpdateTaskStatusRequest request,
CancellationToken cancellationToken = default)
{
var command = new UpdateTaskStatusCommand
{
TaskId = id,
NewStatus = request.NewStatus
};
var result = await _mediator.Send(command, cancellationToken);
return Ok(result);
}
}
/// <summary>
/// Request model for creating a task
/// </summary>
public record CreateTaskRequest
{
public string Title { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public string Priority { get; init; } = "Medium";
public decimal? EstimatedHours { get; init; }
public Guid? AssigneeId { get; init; }
public Guid CreatedBy { get; init; }
}
/// <summary>
/// Request model for updating a task
/// </summary>
public record UpdateTaskRequest
{
public string Title { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public string? Status { get; init; }
public string? Priority { get; init; }
public decimal? EstimatedHours { get; init; }
public Guid? AssigneeId { get; init; }
}
/// <summary>
/// Request model for assigning a task
/// </summary>
public record AssignTaskRequest
{
public Guid? AssigneeId { get; init; }
}
/// <summary>
/// Request model for updating task status
/// </summary>
public record UpdateTaskStatusRequest
{
public string NewStatus { get; init; } = string.Empty;
}