mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-06 18:12:11 +00:00
feat(dashboard): add core dashboard metrics (SH-343)
This commit is contained in:
parent
efd13b6f60
commit
a2e749c72d
9 changed files with 331 additions and 12 deletions
|
|
@ -4,6 +4,7 @@ using FluentAssertions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SeaHaven.DataServices.Implementation;
|
using SeaHaven.DataServices.Implementation;
|
||||||
using SeaHaven.Services.Exceptions;
|
using SeaHaven.Services.Exceptions;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
using SeaHaven.Services.Helpers;
|
using SeaHaven.Services.Helpers;
|
||||||
using SeaHaven.Services.Implementation;
|
using SeaHaven.Services.Implementation;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -28,12 +29,16 @@ public class DashboardServiceTests
|
||||||
return new DashboardService(new DashboardDataService(ctx), resolver);
|
return new DashboardService(new DashboardDataService(ctx), resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ClaimsPrincipal AccountUser(int accountId)
|
private static ClaimsPrincipal AccountUser(
|
||||||
|
int accountId,
|
||||||
|
string userId = "dispatcher-1",
|
||||||
|
string role = "Admin")
|
||||||
{
|
{
|
||||||
var claims = new[]
|
var claims = new[]
|
||||||
{
|
{
|
||||||
new Claim(SeaHavenClaimTypes.AccountId, accountId.ToString()),
|
new Claim(SeaHavenClaimTypes.AccountId, accountId.ToString()),
|
||||||
new Claim(ClaimTypes.Role, "Dispatcher")
|
new Claim(ClaimTypes.NameIdentifier, userId),
|
||||||
|
new Claim(ClaimTypes.Role, role)
|
||||||
};
|
};
|
||||||
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +71,8 @@ public class DashboardServiceTests
|
||||||
);
|
);
|
||||||
ctx.SaveChanges();
|
ctx.SaveChanges();
|
||||||
|
|
||||||
var stats = await NewService(ctx).GetStatsAsync(AccountUser(1), CancellationToken.None);
|
var stats = await NewService(ctx).GetStatsAsync(
|
||||||
|
AccountUser(1), new DashboardStatsQueryDTO(), CancellationToken.None);
|
||||||
|
|
||||||
stats.Total.Should().Be(7);
|
stats.Total.Should().Be(7);
|
||||||
stats.Open.Should().Be(4);
|
stats.Open.Should().Be(4);
|
||||||
|
|
@ -84,7 +90,8 @@ public class DashboardServiceTests
|
||||||
new WorkOrder { Status = "Open", istemplate = true });
|
new WorkOrder { Status = "Open", istemplate = true });
|
||||||
ctx.SaveChanges();
|
ctx.SaveChanges();
|
||||||
|
|
||||||
var stats = await NewService(ctx).GetStatsAsync(OrgWideUser(), CancellationToken.None);
|
var stats = await NewService(ctx).GetStatsAsync(
|
||||||
|
OrgWideUser(), new DashboardStatsQueryDTO(), CancellationToken.None);
|
||||||
|
|
||||||
stats.Total.Should().Be(2);
|
stats.Total.Should().Be(2);
|
||||||
stats.Open.Should().Be(1);
|
stats.Open.Should().Be(1);
|
||||||
|
|
@ -100,10 +107,88 @@ public class DashboardServiceTests
|
||||||
new Claim(ClaimTypes.Role, "Dispatcher")
|
new Claim(ClaimTypes.Role, "Dispatcher")
|
||||||
}, "test"));
|
}, "test"));
|
||||||
|
|
||||||
var act = () => NewService(ctx).GetStatsAsync(user, CancellationToken.None);
|
var act = () => NewService(ctx).GetStatsAsync(
|
||||||
|
user, new DashboardStatsQueryDTO(), CancellationToken.None);
|
||||||
|
|
||||||
var exception = await act.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
var exception = await act.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
||||||
|
|
||||||
exception.Which.Code.Should().Be("Forbidden");
|
exception.Which.Code.Should().Be("Forbidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetStatsAsync_UsesDashboardMetricRulesForDateRange()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var today = DashboardBusinessTime.Today();
|
||||||
|
ctx.workOrders.AddRange(
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
AccountId = 1,
|
||||||
|
WorkOrderType = Data.SeaHavenIndustries.Enums.WorkOrderType.PM,
|
||||||
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Scheduled,
|
||||||
|
ScheduledDate = today.ToDateTime(TimeOnly.MinValue),
|
||||||
|
OriginalDate = today.AddDays(-6)
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
AccountId = 1,
|
||||||
|
WorkOrderType = Data.SeaHavenIndustries.Enums.WorkOrderType.Emergency,
|
||||||
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.InProgress,
|
||||||
|
ScheduledDate = today.AddDays(-3).ToDateTime(TimeOnly.MinValue)
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
AccountId = 1,
|
||||||
|
WorkOrderType = Data.SeaHavenIndustries.Enums.WorkOrderType.Reactive,
|
||||||
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Completed,
|
||||||
|
ScheduledDate = today.ToDateTime(TimeOnly.MinValue),
|
||||||
|
OriginalDate = today.AddDays(-15),
|
||||||
|
CompletedDate = today.AddDays(-12).ToDateTime(TimeOnly.MinValue)
|
||||||
|
},
|
||||||
|
new WorkOrder
|
||||||
|
{
|
||||||
|
AccountId = 1,
|
||||||
|
WorkOrderType = Data.SeaHavenIndustries.Enums.WorkOrderType.PM,
|
||||||
|
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Incomplete,
|
||||||
|
ScheduledDate = today.AddDays(4).ToDateTime(TimeOnly.MinValue)
|
||||||
|
});
|
||||||
|
ctx.SaveChanges();
|
||||||
|
|
||||||
|
var query = new DashboardStatsQueryDTO
|
||||||
|
{
|
||||||
|
DateFrom = today.AddDays(-10),
|
||||||
|
DateTo = today.AddDays(10)
|
||||||
|
};
|
||||||
|
var stats = await NewService(ctx).GetStatsAsync(
|
||||||
|
AccountUser(1), query, CancellationToken.None);
|
||||||
|
|
||||||
|
stats.Total.Should().Be(4);
|
||||||
|
stats.Breakdown.Overdue.Should().Be(1);
|
||||||
|
stats.Breakdown.Other.Should().Be(0);
|
||||||
|
stats.Breakdown.PM.Should().Be(2);
|
||||||
|
stats.Breakdown.Emergency.Should().Be(0);
|
||||||
|
stats.Breakdown.Reactive.Should().Be(1);
|
||||||
|
(stats.Breakdown.PM + stats.Breakdown.Emergency + stats.Breakdown.Reactive
|
||||||
|
+ stats.Breakdown.Overdue + stats.Breakdown.Other).Should().Be(stats.Total);
|
||||||
|
stats.DueCount.Should().Be(3);
|
||||||
|
stats.CompletedDueCount.Should().Be(1);
|
||||||
|
stats.CompletionRate.Should().Be(33.33m);
|
||||||
|
stats.AverageResolutionDays.Should().Be(3m);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetStatsAsync_DispatcherOnlySeesAssignedOrders()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
ctx.workOrders.AddRange(
|
||||||
|
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-1" },
|
||||||
|
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-2" });
|
||||||
|
ctx.SaveChanges();
|
||||||
|
|
||||||
|
var stats = await NewService(ctx).GetStatsAsync(
|
||||||
|
AccountUser(1, role: "Dispatcher"), new DashboardStatsQueryDTO(), CancellationToken.None);
|
||||||
|
|
||||||
|
stats.Total.Should().Be(1);
|
||||||
|
stats.Open.Should().Be(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
using SeaHaven.Services.Interfaces;
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
namespace Api.SeaHavenIndustries.Controllers
|
namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
|
@ -17,16 +18,23 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("Stats")]
|
[HttpGet("Stats")]
|
||||||
public async Task<IActionResult> GetStats(CancellationToken cancellationToken)
|
public async Task<IActionResult> GetStats(
|
||||||
|
[FromQuery] DashboardStatsQueryDTO query,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var stats = await _dashboardService.GetStatsAsync(User, cancellationToken);
|
var stats = await _dashboardService.GetStatsAsync(User, query, cancellationToken);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
total = stats.Total,
|
total = stats.Total,
|
||||||
open = stats.Open,
|
open = stats.Open,
|
||||||
notDispatched = stats.NotDispatched,
|
notDispatched = stats.NotDispatched,
|
||||||
completed = stats.Completed
|
completed = stats.Completed,
|
||||||
|
breakdown = stats.Breakdown,
|
||||||
|
dueCount = stats.DueCount,
|
||||||
|
completedDueCount = stats.CompletedDueCount,
|
||||||
|
completionRate = stats.CompletionRate,
|
||||||
|
averageResolutionDays = stats.AverageResolutionDays
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Helpers;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Implementation
|
namespace SeaHaven.DataServices.Implementation
|
||||||
|
|
@ -36,5 +38,40 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
Completed = completed
|
Completed = completed
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<DashboardWorkOrder>> GetDashboardWorkOrdersAsync(
|
||||||
|
int? accountId,
|
||||||
|
string? dispatcherId,
|
||||||
|
DateOnly? dateFrom,
|
||||||
|
DateOnly? dateTo,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||||
|
if (accountId is int scopedAccountId)
|
||||||
|
query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, scopedAccountId);
|
||||||
|
|
||||||
|
if (dispatcherId != null)
|
||||||
|
query = query.Where(w => w.AssignTo == dispatcherId);
|
||||||
|
|
||||||
|
if (dateFrom is DateOnly from && dateTo is DateOnly to)
|
||||||
|
{
|
||||||
|
var fromDate = from.ToDateTime(TimeOnly.MinValue);
|
||||||
|
var toDateExclusive = to.AddDays(1).ToDateTime(TimeOnly.MinValue);
|
||||||
|
query = query.Where(w =>
|
||||||
|
w.ScheduledDate >= fromDate && w.ScheduledDate < toDateExclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.Select(w => new DashboardWorkOrder(
|
||||||
|
w.Id,
|
||||||
|
w.AssignTo,
|
||||||
|
w.WorkOrderType,
|
||||||
|
w.LifecycleStatus,
|
||||||
|
w.LegacyStatus ?? w.Status,
|
||||||
|
w.ScheduledDate,
|
||||||
|
w.OriginalDate,
|
||||||
|
w.CompletedDate))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,31 @@
|
||||||
namespace SeaHaven.DataServices.Interfaces
|
namespace SeaHaven.DataServices.Interfaces
|
||||||
{
|
{
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
|
||||||
public interface IDashboardDataService
|
public interface IDashboardDataService
|
||||||
{
|
{
|
||||||
Task<DashboardCounts> GetWorkOrderCountsAsync(
|
Task<DashboardCounts> GetWorkOrderCountsAsync(
|
||||||
int? accountId,
|
int? accountId,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<DashboardWorkOrder>> GetDashboardWorkOrdersAsync(
|
||||||
|
int? accountId,
|
||||||
|
string? dispatcherId,
|
||||||
|
DateOnly? dateFrom,
|
||||||
|
DateOnly? dateTo,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record DashboardWorkOrder(
|
||||||
|
int Id,
|
||||||
|
string? AssignTo,
|
||||||
|
WorkOrderType? WorkOrderType,
|
||||||
|
LifecycleStatus? LifecycleStatus,
|
||||||
|
string? LegacyStatus,
|
||||||
|
DateTime? ScheduledDate,
|
||||||
|
DateOnly? OriginalDate,
|
||||||
|
DateTime? CompletedDate);
|
||||||
|
|
||||||
public class DashboardCounts
|
public class DashboardCounts
|
||||||
{
|
{
|
||||||
public int Total { get; set; }
|
public int Total { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,30 @@
|
||||||
namespace SeaHaven.Services.DTOs
|
namespace SeaHaven.Services.DTOs
|
||||||
{
|
{
|
||||||
|
public sealed class DashboardStatsQueryDTO
|
||||||
|
{
|
||||||
|
public DateOnly? DateFrom { get; set; }
|
||||||
|
public DateOnly? DateTo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class DashboardStatsDTO
|
public class DashboardStatsDTO
|
||||||
{
|
{
|
||||||
public int Total { get; set; }
|
public int Total { get; set; }
|
||||||
public int Open { get; set; }
|
public int Open { get; set; }
|
||||||
public int NotDispatched { get; set; }
|
public int NotDispatched { get; set; }
|
||||||
public int Completed { get; set; }
|
public int Completed { get; set; }
|
||||||
|
public DashboardBreakdownDTO Breakdown { get; set; } = new();
|
||||||
|
public int DueCount { get; set; }
|
||||||
|
public int CompletedDueCount { get; set; }
|
||||||
|
public decimal CompletionRate { get; set; }
|
||||||
|
public decimal? AverageResolutionDays { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DashboardBreakdownDTO
|
||||||
|
{
|
||||||
|
public int PM { get; set; }
|
||||||
|
public int Emergency { get; set; }
|
||||||
|
public int Reactive { get; set; }
|
||||||
|
public int Overdue { get; set; }
|
||||||
|
public int Other { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
SeaHaven.Services/Helpers/DashboardBusinessTime.cs
Normal file
13
SeaHaven.Services/Helpers/DashboardBusinessTime.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace SeaHaven.Services.Helpers;
|
||||||
|
|
||||||
|
public static class DashboardBusinessTime
|
||||||
|
{
|
||||||
|
public const string TimeZoneId = "America/New_York";
|
||||||
|
|
||||||
|
public static DateOnly Today()
|
||||||
|
{
|
||||||
|
var timezone = TimeZoneInfo.FindSystemTimeZoneById(TimeZoneId);
|
||||||
|
var businessNow = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, timezone);
|
||||||
|
return DateOnly.FromDateTime(businessNow);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
SeaHaven.Services/Helpers/DashboardMetrics.cs
Normal file
113
SeaHaven.Services/Helpers/DashboardMetrics.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Exceptions;
|
||||||
|
using SeaHaven.Services.Helpers;
|
||||||
using SeaHaven.Services.Interfaces;
|
using SeaHaven.Services.Interfaces;
|
||||||
|
|
||||||
namespace SeaHaven.Services.Implementation
|
namespace SeaHaven.Services.Implementation
|
||||||
|
|
@ -20,17 +22,38 @@ namespace SeaHaven.Services.Implementation
|
||||||
|
|
||||||
public async Task<DashboardStatsDTO> GetStatsAsync(
|
public async Task<DashboardStatsDTO> GetStatsAsync(
|
||||||
ClaimsPrincipal user,
|
ClaimsPrincipal user,
|
||||||
|
DashboardStatsQueryDTO query,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var accountId = _accountResolver.ResolveAccountFilter(user);
|
var accountId = _accountResolver.ResolveAccountFilter(user);
|
||||||
|
var dispatcherId = user.IsInRole("Dispatcher")
|
||||||
|
? user.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||||
|
: null;
|
||||||
|
if (user.IsInRole("Dispatcher") && string.IsNullOrWhiteSpace(dispatcherId))
|
||||||
|
throw new WorkOrderBoardValidationException("Forbidden", "Dispatcher identity is required.");
|
||||||
|
|
||||||
var counts = await _dataService.GetWorkOrderCountsAsync(accountId, cancellationToken);
|
var counts = await _dataService.GetWorkOrderCountsAsync(accountId, cancellationToken);
|
||||||
|
var workOrders = await _dataService.GetDashboardWorkOrdersAsync(
|
||||||
|
accountId,
|
||||||
|
dispatcherId,
|
||||||
|
query.DateFrom,
|
||||||
|
query.DateTo,
|
||||||
|
cancellationToken);
|
||||||
|
var metrics = DashboardMetrics.Calculate(workOrders, DashboardBusinessTime.Today());
|
||||||
|
|
||||||
|
var useMetrics = query.DateFrom.HasValue && query.DateTo.HasValue || dispatcherId is not null;
|
||||||
|
|
||||||
return new DashboardStatsDTO
|
return new DashboardStatsDTO
|
||||||
{
|
{
|
||||||
Total = counts.Total,
|
Total = useMetrics ? metrics.Total : counts.Total,
|
||||||
Open = counts.Open,
|
Open = useMetrics ? metrics.Open : counts.Open,
|
||||||
NotDispatched = counts.NotDispatched,
|
NotDispatched = useMetrics ? metrics.NotDispatched : counts.NotDispatched,
|
||||||
Completed = counts.Completed
|
Completed = useMetrics ? metrics.Completed : counts.Completed,
|
||||||
|
Breakdown = metrics.Breakdown,
|
||||||
|
DueCount = metrics.DueCount,
|
||||||
|
CompletedDueCount = metrics.CompletedDueCount,
|
||||||
|
CompletionRate = metrics.CompletionRate,
|
||||||
|
AverageResolutionDays = metrics.AverageResolutionDays
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ namespace SeaHaven.Services.Interfaces
|
||||||
{
|
{
|
||||||
Task<DashboardStatsDTO> GetStatsAsync(
|
Task<DashboardStatsDTO> GetStatsAsync(
|
||||||
ClaimsPrincipal user,
|
ClaimsPrincipal user,
|
||||||
|
DashboardStatsQueryDTO query,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue