Project Init
This commit is contained in:
@@ -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": []
|
||||||
|
|||||||
116
colaflow-api/src/ColaFlow.API/Controllers/EpicsController.cs
Normal file
116
colaflow-api/src/ColaFlow.API/Controllers/EpicsController.cs
Normal 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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>>;
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user