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;
+ }
+}