using Data.SeaHavenIndustries.Enums; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; namespace SeaHaven.Services.Helpers; public static class DashboardMetrics { public static DashboardMetricResult Calculate( IReadOnlyList workOrders, DateOnly today) { var breakdown = new DashboardBreakdownDTO(); var dueCount = 0; var completedDueCount = 0; var resolutionDays = new List(); foreach (var workOrder in workOrders) { var status = workOrder.LifecycleStatus ?? LifecycleStatusMapper.ParseLifecycleStatus(workOrder.LegacyStatus); var completed = status == LifecycleStatus.Completed; var canceled = status == LifecycleStatus.Canceled; AddBreakdownCount(breakdown, workOrder.WorkOrderType); var due = completed || (workOrder.ScheduledDate is DateTime scheduled && DateOnly.FromDateTime(scheduled.Date) <= today); if (due) { dueCount++; if (completed) completedDueCount++; } if (completed && workOrder.CompletedDate is DateTime completedDate) { var anchor = workOrder.OriginalDate ?? (workOrder.ScheduledDate is DateTime scheduledDate ? DateOnly.FromDateTime(scheduledDate.Date) : null); if (anchor is DateOnly originalDate) resolutionDays.Add((decimal)(completedDate.Date - originalDate.ToDateTime(TimeOnly.MinValue)).TotalDays); } } return new DashboardMetricResult( workOrders.Count, workOrders.Count(workOrder => { var status = workOrder.LifecycleStatus ?? LifecycleStatusMapper.ParseLifecycleStatus(workOrder.LegacyStatus); return status is not LifecycleStatus.Completed and not LifecycleStatus.Canceled; }), workOrders.Count(workOrder => string.IsNullOrWhiteSpace(workOrder.AssignTo)), workOrders.Count(workOrder => (workOrder.LifecycleStatus ?? LifecycleStatusMapper.ParseLifecycleStatus(workOrder.LegacyStatus)) == LifecycleStatus.Completed), breakdown, dueCount, completedDueCount, dueCount == 0 ? 0 : Math.Round((decimal)completedDueCount / dueCount * 100, 2), resolutionDays.Count == 0 ? null : Math.Round(resolutionDays.Average(), 2), BuildStatusDistribution(workOrders)); } private static IReadOnlyList BuildStatusDistribution( IReadOnlyList workOrders) => workOrders .GroupBy(workOrder => workOrder.LifecycleStatus ?? LifecycleStatusMapper.ParseLifecycleStatus(workOrder.LegacyStatus)) .Select(group => new DashboardStatusBucketDTO { Status = group.Key.HasValue ? LifecycleStatusMapper.ToFeLabel(group.Key.Value) : "Unknown", Count = group.Count() }) .OrderByDescending(bucket => bucket.Count) .ThenBy(bucket => bucket.Status, StringComparer.OrdinalIgnoreCase) .ToList(); // Partition by the stored type only; past due is an overlay on any type, not a bucket. private static void AddBreakdownCount( DashboardBreakdownDTO breakdown, WorkOrderType? type) { switch (type) { case WorkOrderType.PM: breakdown.PM++; break; case WorkOrderType.Emergency: breakdown.Emergency++; break; case WorkOrderType.Reactive: breakdown.Reactive++; break; case WorkOrderType.Overdue: breakdown.Overdue++; break; default: breakdown.Other++; break; } } } public sealed record DashboardMetricResult( int Total, int Open, int NotDispatched, int Completed, DashboardBreakdownDTO Breakdown, int DueCount, int CompletedDueCount, decimal CompletionRate, decimal? AverageResolutionDays, IReadOnlyList StatusDistribution);