152 lines
5.9 KiB
C#
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;
|
|
}
|
|
}
|