diff --git a/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs b/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs index dab400f..0dd6c8c 100644 --- a/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs +++ b/colaflow-api/src/ColaFlow.API/Controllers/SprintsController.cs @@ -11,6 +11,7 @@ using ColaFlow.Modules.ProjectManagement.Application.Commands.RemoveTaskFromSpri using ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintById; using ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintsByProjectId; using ColaFlow.Modules.ProjectManagement.Application.Queries.GetActiveSprints; +using ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintBurndown; using ColaFlow.Modules.ProjectManagement.Application.DTOs; namespace ColaFlow.API.Controllers; @@ -134,4 +135,16 @@ public class SprintsController : ControllerBase await _mediator.Send(new RemoveTaskFromSprintCommand(id, taskId)); return NoContent(); } + + /// + /// Get burndown chart data for a sprint + /// + [HttpGet("{id}/burndown")] + public async Task> GetBurndown(Guid id) + { + var result = await _mediator.Send(new GetSprintBurndownQuery(id)); + if (result == null) + return NotFound(); + return Ok(result); + } } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/BurndownChartDto.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/BurndownChartDto.cs new file mode 100644 index 0000000..3069e29 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/BurndownChartDto.cs @@ -0,0 +1,24 @@ +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintBurndown; + +/// +/// Burndown Chart Data Transfer Object +/// +public record BurndownChartDto( + Guid SprintId, + string SprintName, + DateTime StartDate, + DateTime EndDate, + int TotalStoryPoints, + int RemainingStoryPoints, + double CompletionPercentage, + List IdealBurndown, + List ActualBurndown +); + +/// +/// Single data point in the burndown chart +/// +public record BurndownDataPoint( + DateTime Date, + int StoryPoints +); diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQuery.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQuery.cs new file mode 100644 index 0000000..2df4824 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintBurndown; + +/// +/// Query to get burndown chart data for a sprint +/// +public record GetSprintBurndownQuery(Guid SprintId) : IRequest; diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs new file mode 100644 index 0000000..a073fb4 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs @@ -0,0 +1,157 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; +using ColaFlow.Modules.ProjectManagement.Domain.Repositories; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; + +namespace ColaFlow.Modules.ProjectManagement.Application.Queries.GetSprintBurndown; + +/// +/// Handler for GetSprintBurndownQuery +/// Calculates ideal and actual burndown data for sprint progress visualization +/// +public sealed class GetSprintBurndownQueryHandler : IRequestHandler +{ + private readonly IProjectRepository _projectRepository; + private readonly IApplicationDbContext _context; + private readonly ILogger _logger; + + public GetSprintBurndownQueryHandler( + IProjectRepository projectRepository, + IApplicationDbContext context, + ILogger logger) + { + _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); + _context = context ?? throw new ArgumentNullException(nameof(context)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task Handle(GetSprintBurndownQuery request, CancellationToken cancellationToken) + { + // 1. Get Sprint + var sprintId = SprintId.From(request.SprintId); + var sprint = await _projectRepository.GetSprintByIdReadOnlyAsync(sprintId, cancellationToken); + + if (sprint == null) + { + _logger.LogWarning("Sprint not found: {SprintId}", request.SprintId); + return null; + } + + // 2. Get all tasks in this sprint + var taskIds = sprint.TaskIds.Select(t => t.Value).ToList(); + var tasks = await _context.Tasks + .Where(t => taskIds.Contains(t.Id.Value)) + .ToListAsync(cancellationToken); + + // 3. Calculate total story points (simplified: use EstimatedHours as story points for MVP) + // In Phase 2, add StoryPoints property to WorkTask + var totalPoints = tasks.Count; // Simple count for MVP + var completedTasks = tasks.Where(t => t.Status.Name == "Done").ToList(); + var remainingPoints = totalPoints - completedTasks.Count; + + // 4. Calculate ideal burndown (linear) + var idealBurndown = CalculateIdealBurndown(sprint.StartDate, sprint.EndDate, totalPoints); + + // 5. Calculate actual burndown (based on task completion dates) + var actualBurndown = CalculateActualBurndown(sprint.StartDate, sprint.EndDate, tasks, totalPoints); + + // 6. Calculate completion percentage + var completionPercentage = totalPoints > 0 + ? Math.Round((double)(totalPoints - remainingPoints) / totalPoints * 100, 2) + : 0; + + _logger.LogInformation( + "Calculated burndown for Sprint {SprintId}: Total={Total}, Remaining={Remaining}, Completion={Completion}%", + request.SprintId, totalPoints, remainingPoints, completionPercentage); + + return new BurndownChartDto( + sprint.Id.Value, + sprint.Name, + sprint.StartDate, + sprint.EndDate, + totalPoints, + remainingPoints, + completionPercentage, + idealBurndown, + actualBurndown + ); + } + + /// + /// Calculate ideal burndown - linear decrease from total to 0 + /// + private List CalculateIdealBurndown(DateTime startDate, DateTime endDate, int totalPoints) + { + var dataPoints = new List(); + var totalDays = (endDate.Date - startDate.Date).Days; + + if (totalDays <= 0) + { + dataPoints.Add(new BurndownDataPoint(startDate.Date, totalPoints)); + return dataPoints; + } + + var pointsPerDay = (double)totalPoints / totalDays; + + for (int day = 0; day <= totalDays; day++) + { + var date = startDate.Date.AddDays(day); + var remaining = totalPoints - (int)Math.Round(pointsPerDay * day); + dataPoints.Add(new BurndownDataPoint(date, Math.Max(0, remaining))); + } + + return dataPoints; + } + + /// + /// Calculate actual burndown based on task completion dates + /// MVP: Uses UpdatedAt timestamp as completion date approximation + /// Phase 2: Use audit logs for exact completion dates + /// + private List CalculateActualBurndown( + DateTime startDate, + DateTime endDate, + List tasks, + int totalPoints) + { + var dataPoints = new List(); + var currentDate = startDate.Date; + var finalDate = DateTime.UtcNow.Date < endDate.Date ? DateTime.UtcNow.Date : endDate.Date; + + // Get completed tasks with their completion dates (approximated by UpdatedAt) + var completedTasks = tasks + .Where(t => t.Status.Name == "Done" && t.UpdatedAt.HasValue) + .Select(t => new + { + Task = t, + CompletedDate = t.UpdatedAt!.Value.Date + }) + .OrderBy(t => t.CompletedDate) + .ToList(); + + var remainingPoints = totalPoints; + + // Generate daily data points + while (currentDate <= finalDate) + { + // Count tasks completed by this date (before end of day) + var completedByDate = completedTasks + .Count(tc => tc.CompletedDate <= currentDate); + + remainingPoints = totalPoints - completedByDate; + + dataPoints.Add(new BurndownDataPoint(currentDate, Math.Max(0, remainingPoints))); + currentDate = currentDate.AddDays(1); + } + + // If no data points, add at least start point + if (dataPoints.Count == 0) + { + dataPoints.Add(new BurndownDataPoint(startDate.Date, totalPoints)); + } + + return dataPoints; + } +}