shoc-backend/SeaHaven.Services/Helpers/DashboardMetrics.cs
2026-09-16 21:37:17 -03:00

131 lines
4.7 KiB
C#

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<DashboardWorkOrder> workOrders,
DateOnly today)
{
var breakdown = new DashboardBreakdownDTO();
var dueCount = 0;
var completedDueCount = 0;
var resolutionDays = new List<decimal>();
foreach (var workOrder in workOrders)
{
var status = workOrder.LifecycleStatus
?? LifecycleStatusMapper.ParseLifecycleStatus(workOrder.LegacyStatus);
var completed = status == LifecycleStatus.Completed;
var canceled = status == LifecycleStatus.Canceled;
var pastDue = IsPastDue(workOrder, status, today);
AddBreakdownCount(breakdown, workOrder.WorkOrderType, pastDue);
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<DashboardStatusBucketDTO> BuildStatusDistribution(
IReadOnlyList<DashboardWorkOrder> 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();
private static bool IsPastDue(
DashboardWorkOrder workOrder,
LifecycleStatus? status,
DateOnly today)
=> workOrder.ScheduledDate is DateTime scheduled
&& DateOnly.FromDateTime(scheduled.Date) < today
&& status is not LifecycleStatus.Completed and not LifecycleStatus.Canceled;
private static void AddBreakdownCount(
DashboardBreakdownDTO breakdown,
WorkOrderType? type,
bool pastDue)
{
if (pastDue)
{
breakdown.Overdue++;
return;
}
switch (type)
{
case WorkOrderType.PM:
breakdown.PM++;
break;
case WorkOrderType.Emergency:
breakdown.Emergency++;
break;
case WorkOrderType.Reactive:
breakdown.Reactive++;
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<DashboardStatusBucketDTO> StatusDistribution);