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( IProjectRepository projectRepository, IApplicationDbContext context, ILogger logger) : IRequestHandler { private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); private readonly IApplicationDbContext _context = context ?? throw new ArgumentNullException(nameof(context)); private readonly ILogger _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; } }