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);