Files
ColaFlow/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Application/Queries/GetSprintBurndown/GetSprintBurndownQueryHandler.cs
Yaojia Wang 63ff1a9914
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Clean up
2025-11-09 18:40:36 +01:00

152 lines
5.9 KiB
C#

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;
/// <summary>
/// Handler for GetSprintBurndownQuery
/// Calculates ideal and actual burndown data for sprint progress visualization
/// </summary>
public sealed class GetSprintBurndownQueryHandler(
IProjectRepository projectRepository,
IApplicationDbContext context,
ILogger<GetSprintBurndownQueryHandler> logger)
: IRequestHandler<GetSprintBurndownQuery, BurndownChartDto?>
{
private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context));
private readonly ILogger<GetSprintBurndownQueryHandler> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
public async Task<BurndownChartDto?> 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
);
}
/// <summary>
/// Calculate ideal burndown - linear decrease from total to 0
/// </summary>
private List<BurndownDataPoint> CalculateIdealBurndown(DateTime startDate, DateTime endDate, int totalPoints)
{
var dataPoints = new List<BurndownDataPoint>();
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;
}
/// <summary>
/// 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
/// </summary>
private List<BurndownDataPoint> CalculateActualBurndown(
DateTime startDate,
DateTime endDate,
List<Domain.Aggregates.ProjectAggregate.WorkTask> tasks,
int totalPoints)
{
var dataPoints = new List<BurndownDataPoint>();
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;
}
}