From 8caf8c1bcf4a5ff4da45567950e12955522500a9 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Mon, 3 Nov 2025 00:04:19 +0100 Subject: [PATCH] Project Init --- .claude/settings.local.json | 4 +- .../Controllers/EpicsController.cs | 116 ++++++++++++++++++ colaflow-api/src/ColaFlow.API/Program.cs | 14 +++ .../GetEpicById/GetEpicByIdQueryHandler.cs | 62 ++++++++++ .../GetEpicsByProjectIdQuery.cs | 9 ++ .../GetEpicsByProjectIdQueryHandler.cs | 43 +++++++ .../Repositories/ProjectRepository.cs | 29 +++++ 7 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 colaflow-api/src/ColaFlow.API/Controllers/EpicsController.cs create mode 100644 colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicById/GetEpicByIdQueryHandler.cs create mode 100644 colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicsByProjectId/GetEpicsByProjectIdQuery.cs create mode 100644 colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicsByProjectId/GetEpicsByProjectIdQueryHandler.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 516cf8b..4869593 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -50,7 +50,9 @@ "Bash(git rm:*)", "Bash(rm:*)", "Bash(git reset:*)", - "Bash(git commit:*)" + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(npm run dev:*)" ], "deny": [], "ask": [] diff --git a/colaflow-api/src/ColaFlow.API/Controllers/EpicsController.cs b/colaflow-api/src/ColaFlow.API/Controllers/EpicsController.cs new file mode 100644 index 0000000..8664640 --- /dev/null +++ b/colaflow-api/src/ColaFlow.API/Controllers/EpicsController.cs @@ -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; + +/// +/// Epics API Controller +/// +[ApiController] +[Route("api/v1")] +public class EpicsController : ControllerBase +{ + private readonly IMediator _mediator; + + public EpicsController(IMediator mediator) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + } + + /// + /// Get all epics for a project + /// + [HttpGet("projects/{projectId:guid}/epics")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetProjectEpics(Guid projectId, CancellationToken cancellationToken = default) + { + var query = new GetEpicsByProjectIdQuery(projectId); + var result = await _mediator.Send(query, cancellationToken); + return Ok(result); + } + + /// + /// Get epic by ID + /// + [HttpGet("epics/{id:guid}")] + [ProducesResponseType(typeof(EpicDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetEpic(Guid id, CancellationToken cancellationToken = default) + { + var query = new GetEpicByIdQuery(id); + var result = await _mediator.Send(query, cancellationToken); + return Ok(result); + } + + /// + /// Create a new epic + /// + [HttpPost("projects/{projectId:guid}/epics")] + [ProducesResponseType(typeof(EpicDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task 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); + } + + /// + /// Update an existing epic + /// + [HttpPut("epics/{id:guid}")] + [ProducesResponseType(typeof(EpicDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task 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); + } +} + +/// +/// Request model for creating an epic +/// +public record CreateEpicRequest +{ + public string Name { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; + public Guid CreatedBy { get; init; } +} + +/// +/// Request model for updating an epic +/// +public record UpdateEpicRequest +{ + public string Name { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; +} diff --git a/colaflow-api/src/ColaFlow.API/Program.cs b/colaflow-api/src/ColaFlow.API/Program.cs index b0c4e1e..129df08 100644 --- a/colaflow-api/src/ColaFlow.API/Program.cs +++ b/colaflow-api/src/ColaFlow.API/Program.cs @@ -10,6 +10,17 @@ builder.Services.AddProjectManagementModule(builder.Configuration); // Add controllers 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 builder.Services.AddOpenApi(); @@ -25,6 +36,9 @@ if (app.Environment.IsDevelopment()) // Global exception handler (should be first in pipeline) app.UseMiddleware(); +// Enable CORS +app.UseCors("AllowFrontend"); + app.UseHttpsRedirection(); app.MapControllers(); diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicById/GetEpicByIdQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicById/GetEpicByIdQueryHandler.cs new file mode 100644 index 0000000..9cd6066 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicById/GetEpicByIdQueryHandler.cs @@ -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; + +/// +/// Handler for GetEpicByIdQuery +/// +public sealed class GetEpicByIdQueryHandler : IRequestHandler +{ + private readonly IProjectRepository _projectRepository; + + public GetEpicByIdQueryHandler(IProjectRepository projectRepository) + { + _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); + } + + public async Task 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() + }).ToList() + }; + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicsByProjectId/GetEpicsByProjectIdQuery.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicsByProjectId/GetEpicsByProjectIdQuery.cs new file mode 100644 index 0000000..61ea099 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicsByProjectId/GetEpicsByProjectIdQuery.cs @@ -0,0 +1,9 @@ +using MediatR; +using ColaFlow.Modules.ProjectManagement.Application.DTOs; + +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetEpicsByProjectId; + +/// +/// Query to get all Epics for a Project +/// +public sealed record GetEpicsByProjectIdQuery(Guid ProjectId) : IRequest>; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicsByProjectId/GetEpicsByProjectIdQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicsByProjectId/GetEpicsByProjectIdQueryHandler.cs new file mode 100644 index 0000000..1710977 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetEpicsByProjectId/GetEpicsByProjectIdQueryHandler.cs @@ -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; + +/// +/// Handler for GetEpicsByProjectIdQuery +/// +public sealed class GetEpicsByProjectIdQueryHandler : IRequestHandler> +{ + private readonly IProjectRepository _projectRepository; + + public GetEpicsByProjectIdQueryHandler(IProjectRepository projectRepository) + { + _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); + } + + public async Task> 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() // Don't include stories in list view + }).ToList(); + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/ProjectRepository.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/ProjectRepository.cs index 388b0a4..789f6c2 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/ProjectRepository.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/ProjectRepository.cs @@ -38,6 +38,35 @@ public class ProjectRepository : IProjectRepository .ToListAsync(cancellationToken); } + public async Task 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 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 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) { await _context.Projects.AddAsync(project, cancellationToken);