Project Init
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 00:04:19 +01:00
parent 014d62bcc2
commit 8caf8c1bcf
7 changed files with 276 additions and 1 deletions

View File

@@ -50,7 +50,9 @@
"Bash(git rm:*)", "Bash(git rm:*)",
"Bash(rm:*)", "Bash(rm:*)",
"Bash(git reset:*)", "Bash(git reset:*)",
"Bash(git commit:*)" "Bash(git commit:*)",
"Bash(git push:*)",
"Bash(npm run dev:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -0,0 +1,116 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateEpic;
using ColaFlow.Modules.ProjectManagement.Application.Commands.UpdateEpic;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicById;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicsByProjectId;
namespace ColaFlow.API.Controllers;
/// <summary>
/// Epics API Controller
/// </summary>
[ApiController]
[Route("api/v1")]
public class EpicsController : ControllerBase
{
private readonly IMediator _mediator;
public EpicsController(IMediator mediator)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
/// <summary>
/// Get all epics for a project
/// </summary>
[HttpGet("projects/{projectId:guid}/epics")]
[ProducesResponseType(typeof(List<EpicDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetProjectEpics(Guid projectId, CancellationToken cancellationToken = default)
{
var query = new GetEpicsByProjectIdQuery(projectId);
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Get epic by ID
/// </summary>
[HttpGet("epics/{id:guid}")]
[ProducesResponseType(typeof(EpicDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetEpic(Guid id, CancellationToken cancellationToken = default)
{
var query = new GetEpicByIdQuery(id);
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Create a new epic
/// </summary>
[HttpPost("projects/{projectId:guid}/epics")]
[ProducesResponseType(typeof(EpicDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> CreateEpic(
Guid projectId,
[FromBody] CreateEpicRequest request,
CancellationToken cancellationToken = default)
{
var command = new CreateEpicCommand
{
ProjectId = projectId,
Name = request.Name,
Description = request.Description,
CreatedBy = request.CreatedBy
};
var result = await _mediator.Send(command, cancellationToken);
return CreatedAtAction(nameof(GetEpic), new { id = result.Id }, result);
}
/// <summary>
/// Update an existing epic
/// </summary>
[HttpPut("epics/{id:guid}")]
[ProducesResponseType(typeof(EpicDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateEpic(
Guid id,
[FromBody] UpdateEpicRequest request,
CancellationToken cancellationToken = default)
{
var command = new UpdateEpicCommand
{
EpicId = id,
Name = request.Name,
Description = request.Description
};
var result = await _mediator.Send(command, cancellationToken);
return Ok(result);
}
}
/// <summary>
/// Request model for creating an epic
/// </summary>
public record CreateEpicRequest
{
public string Name { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public Guid CreatedBy { get; init; }
}
/// <summary>
/// Request model for updating an epic
/// </summary>
public record UpdateEpicRequest
{
public string Name { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
}

View File

@@ -10,6 +10,17 @@ builder.Services.AddProjectManagementModule(builder.Configuration);
// Add controllers // Add controllers
builder.Services.AddControllers(); builder.Services.AddControllers();
// Configure CORS for frontend
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// Configure OpenAPI/Scalar // Configure OpenAPI/Scalar
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
@@ -25,6 +36,9 @@ if (app.Environment.IsDevelopment())
// Global exception handler (should be first in pipeline) // Global exception handler (should be first in pipeline)
app.UseMiddleware<GlobalExceptionHandlerMiddleware>(); app.UseMiddleware<GlobalExceptionHandlerMiddleware>();
// Enable CORS
app.UseCors("AllowFrontend");
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.MapControllers(); app.MapControllers();

View File

@@ -0,0 +1,62 @@
using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicById;
/// <summary>
/// Handler for GetEpicByIdQuery
/// </summary>
public sealed class GetEpicByIdQueryHandler : IRequestHandler<GetEpicByIdQuery, EpicDto>
{
private readonly IProjectRepository _projectRepository;
public GetEpicByIdQueryHandler(IProjectRepository projectRepository)
{
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
}
public async Task<EpicDto> Handle(GetEpicByIdQuery request, CancellationToken cancellationToken)
{
var epicId = EpicId.From(request.EpicId);
var project = await _projectRepository.GetProjectWithEpicAsync(epicId, cancellationToken);
if (project == null)
throw new NotFoundException("Epic", request.EpicId);
var epic = project.Epics.FirstOrDefault(e => e.Id == epicId);
if (epic == null)
throw new NotFoundException("Epic", request.EpicId);
return new EpicDto
{
Id = epic.Id.Value,
Name = epic.Name,
Description = epic.Description,
ProjectId = epic.ProjectId.Value,
Status = epic.Status.Value,
Priority = epic.Priority.Value,
CreatedBy = epic.CreatedBy.Value,
CreatedAt = epic.CreatedAt,
UpdatedAt = epic.UpdatedAt,
Stories = epic.Stories.Select(s => new StoryDto
{
Id = s.Id.Value,
Title = s.Title,
Description = s.Description,
EpicId = s.EpicId.Value,
Status = s.Status.Value,
Priority = s.Priority.Value,
EstimatedHours = s.EstimatedHours,
ActualHours = s.ActualHours,
AssigneeId = s.AssigneeId?.Value,
CreatedBy = s.CreatedBy.Value,
CreatedAt = s.CreatedAt,
UpdatedAt = s.UpdatedAt,
Tasks = new List<TaskDto>()
}).ToList()
};
}
}

View File

@@ -0,0 +1,9 @@
using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicsByProjectId;
/// <summary>
/// Query to get all Epics for a Project
/// </summary>
public sealed record GetEpicsByProjectIdQuery(Guid ProjectId) : IRequest<List<EpicDto>>;

View File

@@ -0,0 +1,43 @@
using MediatR;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Domain.Exceptions;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicsByProjectId;
/// <summary>
/// Handler for GetEpicsByProjectIdQuery
/// </summary>
public sealed class GetEpicsByProjectIdQueryHandler : IRequestHandler<GetEpicsByProjectIdQuery, List<EpicDto>>
{
private readonly IProjectRepository _projectRepository;
public GetEpicsByProjectIdQueryHandler(IProjectRepository projectRepository)
{
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
}
public async Task<List<EpicDto>> Handle(GetEpicsByProjectIdQuery request, CancellationToken cancellationToken)
{
var projectId = ProjectId.From(request.ProjectId);
var project = await _projectRepository.GetByIdAsync(projectId, cancellationToken);
if (project == null)
throw new NotFoundException("Project", request.ProjectId);
return project.Epics.Select(epic => new EpicDto
{
Id = epic.Id.Value,
Name = epic.Name,
Description = epic.Description,
ProjectId = epic.ProjectId.Value,
Status = epic.Status.Value,
Priority = epic.Priority.Value,
CreatedBy = epic.CreatedBy.Value,
CreatedAt = epic.CreatedAt,
UpdatedAt = epic.UpdatedAt,
Stories = new List<StoryDto>() // Don't include stories in list view
}).ToList();
}
}

View File

@@ -38,6 +38,35 @@ public class ProjectRepository : IProjectRepository
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
public async Task<Project?> GetProjectWithEpicAsync(EpicId epicId, CancellationToken cancellationToken = default)
{
return await _context.Projects
.Include(p => p.Epics)
.ThenInclude(e => e.Stories)
.Where(p => p.Epics.Any(e => e.Id == epicId))
.FirstOrDefaultAsync(cancellationToken);
}
public async Task<Project?> GetProjectWithStoryAsync(StoryId storyId, CancellationToken cancellationToken = default)
{
return await _context.Projects
.Include(p => p.Epics)
.ThenInclude(e => e.Stories)
.ThenInclude(s => s.Tasks)
.Where(p => p.Epics.Any(e => e.Stories.Any(s => s.Id == storyId)))
.FirstOrDefaultAsync(cancellationToken);
}
public async Task<Project?> GetProjectWithTaskAsync(TaskId taskId, CancellationToken cancellationToken = default)
{
return await _context.Projects
.Include(p => p.Epics)
.ThenInclude(e => e.Stories)
.ThenInclude(s => s.Tasks)
.Where(p => p.Epics.Any(e => e.Stories.Any(s => s.Tasks.Any(t => t.Id == taskId))))
.FirstOrDefaultAsync(cancellationToken);
}
public async Task AddAsync(Project project, CancellationToken cancellationToken = default) public async Task AddAsync(Project project, CancellationToken cancellationToken = default)
{ {
await _context.Projects.AddAsync(project, cancellationToken); await _context.Projects.AddAsync(project, cancellationToken);