mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-03 17:53:22 +00:00
feat(dashboard): add regional work order metrics (SH-348)
This commit is contained in:
parent
0194748e3b
commit
d4afcced87
8 changed files with 174 additions and 2 deletions
|
|
@ -301,4 +301,27 @@ public class DashboardServiceTests
|
||||||
result.Items[0].AverageResolutionDays.Should().Be(3m);
|
result.Items[0].AverageResolutionDays.Should().Be(3m);
|
||||||
result.Items[0].Color.Should().Be("#123456");
|
result.Items[0].Color.Should().Be("#123456");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetRegionsAsync_UsesCanonicalBucketsAndUnmappedOther()
|
||||||
|
{
|
||||||
|
await using var ctx = NewContext();
|
||||||
|
ctx.Locations.AddRange(
|
||||||
|
new Locations { Id = 1, State = "NY" },
|
||||||
|
new Locations { Id = 2, State = "ca" },
|
||||||
|
new Locations { Id = 3, State = "XX" });
|
||||||
|
ctx.workOrders.AddRange(
|
||||||
|
new WorkOrder { AccountId = 1, LocationId = 1 },
|
||||||
|
new WorkOrder { AccountId = 1, LocationId = 2 },
|
||||||
|
new WorkOrder { AccountId = 1, LocationId = 3 },
|
||||||
|
new WorkOrder { AccountId = 1, LocationId = null });
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await NewService(ctx).GetRegionsAsync(
|
||||||
|
AccountUser(1), new DashboardStatsQueryDTO(), CancellationToken.None);
|
||||||
|
|
||||||
|
result.Items.Select(row => row.Region).Should().Equal(
|
||||||
|
"East", "Central", "West", "California", "Unmapped/Other");
|
||||||
|
result.Items.Select(row => row.WorkOrderCount).Should().Equal(1, 0, 0, 1, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,5 +89,25 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
return BadRequest(ex.Message);
|
return BadRequest(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("Regions")]
|
||||||
|
public async Task<IActionResult> GetRegions(
|
||||||
|
[FromQuery] DashboardStatsQueryDTO query,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Ok(await _dashboardService.GetRegionsAsync(User, query, cancellationToken));
|
||||||
|
}
|
||||||
|
catch (SeaHaven.Services.Exceptions.WorkOrderBoardValidationException ex)
|
||||||
|
when (ex.Code == "Forbidden")
|
||||||
|
{
|
||||||
|
return StatusCode(StatusCodes.Status403Forbidden, new
|
||||||
|
{
|
||||||
|
code = ex.Code,
|
||||||
|
message = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
w.LegacyStatus ?? w.Status,
|
w.LegacyStatus ?? w.Status,
|
||||||
w.ScheduledDate,
|
w.ScheduledDate,
|
||||||
w.OriginalDate,
|
w.OriginalDate,
|
||||||
w.CompletedDate))
|
w.CompletedDate,
|
||||||
|
w.Locations == null ? null : w.Locations.State))
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
string? LegacyStatus,
|
string? LegacyStatus,
|
||||||
DateTime? ScheduledDate,
|
DateTime? ScheduledDate,
|
||||||
DateOnly? OriginalDate,
|
DateOnly? OriginalDate,
|
||||||
DateTime? CompletedDate);
|
DateTime? CompletedDate,
|
||||||
|
string? LocationState = null);
|
||||||
|
|
||||||
public sealed record DashboardDispatcherWorkload(
|
public sealed record DashboardDispatcherWorkload(
|
||||||
string DispatcherId,
|
string DispatcherId,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,17 @@ namespace SeaHaven.Services.DTOs
|
||||||
public decimal? AverageResolutionDays { get; init; }
|
public decimal? AverageResolutionDays { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class DashboardRegionResponseDTO
|
||||||
|
{
|
||||||
|
public IReadOnlyList<DashboardRegionRowDTO> Items { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DashboardRegionRowDTO
|
||||||
|
{
|
||||||
|
public string Region { get; init; } = string.Empty;
|
||||||
|
public int WorkOrderCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public class DashboardStatsDTO
|
public class DashboardStatsDTO
|
||||||
{
|
{
|
||||||
public int Total { get; set; }
|
public int Total { get; set; }
|
||||||
|
|
|
||||||
84
SeaHaven.Services/Helpers/DashboardRegions.cs
Normal file
84
SeaHaven.Services/Helpers/DashboardRegions.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Helpers;
|
||||||
|
|
||||||
|
public static class DashboardRegions
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlyList<string> Names =
|
||||||
|
["East", "Central", "West", "California", "Unmapped/Other"];
|
||||||
|
|
||||||
|
public static string Resolve(string? state)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(state))
|
||||||
|
return "Unmapped/Other";
|
||||||
|
|
||||||
|
return StateToRegion.TryGetValue(state.Trim().ToUpperInvariant(), out var region)
|
||||||
|
? region
|
||||||
|
: "Unmapped/Other";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, int> Count(
|
||||||
|
IReadOnlyList<DashboardWorkOrder> workOrders)
|
||||||
|
{
|
||||||
|
var counts = Names.ToDictionary(name => name, _ => 0, StringComparer.Ordinal);
|
||||||
|
foreach (var workOrder in workOrders)
|
||||||
|
counts[Resolve(workOrder.LocationState)]++;
|
||||||
|
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> StateToRegion =
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["CT"] = "East",
|
||||||
|
["DE"] = "East",
|
||||||
|
["GA"] = "East",
|
||||||
|
["ME"] = "East",
|
||||||
|
["MD"] = "East",
|
||||||
|
["MA"] = "East",
|
||||||
|
["NH"] = "East",
|
||||||
|
["NJ"] = "East",
|
||||||
|
["NY"] = "East",
|
||||||
|
["NC"] = "East",
|
||||||
|
["PA"] = "East",
|
||||||
|
["RI"] = "East",
|
||||||
|
["SC"] = "East",
|
||||||
|
["VT"] = "East",
|
||||||
|
["VA"] = "East",
|
||||||
|
["WV"] = "East",
|
||||||
|
["AL"] = "Central",
|
||||||
|
["AR"] = "Central",
|
||||||
|
["FL"] = "Central",
|
||||||
|
["IL"] = "Central",
|
||||||
|
["IN"] = "Central",
|
||||||
|
["IA"] = "Central",
|
||||||
|
["KS"] = "Central",
|
||||||
|
["KY"] = "Central",
|
||||||
|
["LA"] = "Central",
|
||||||
|
["MI"] = "Central",
|
||||||
|
["MN"] = "Central",
|
||||||
|
["MS"] = "Central",
|
||||||
|
["MO"] = "Central",
|
||||||
|
["NE"] = "Central",
|
||||||
|
["ND"] = "Central",
|
||||||
|
["OH"] = "Central",
|
||||||
|
["OK"] = "Central",
|
||||||
|
["SD"] = "Central",
|
||||||
|
["TN"] = "Central",
|
||||||
|
["WI"] = "Central",
|
||||||
|
["AK"] = "West",
|
||||||
|
["AZ"] = "West",
|
||||||
|
["CO"] = "West",
|
||||||
|
["HI"] = "West",
|
||||||
|
["ID"] = "West",
|
||||||
|
["MT"] = "West",
|
||||||
|
["NV"] = "West",
|
||||||
|
["NM"] = "West",
|
||||||
|
["OR"] = "West",
|
||||||
|
["TX"] = "West",
|
||||||
|
["UT"] = "West",
|
||||||
|
["WA"] = "West",
|
||||||
|
["WY"] = "West",
|
||||||
|
["CA"] = "California"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -152,5 +152,32 @@ namespace SeaHaven.Services.Implementation
|
||||||
TotalDispatchers = rows.Count
|
TotalDispatchers = rows.Count
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DashboardRegionResponseDTO> GetRegionsAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
DashboardStatsQueryDTO query,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
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 workOrders = await _dataService.GetDashboardWorkOrdersAsync(
|
||||||
|
accountId, dispatcherId, query.DateFrom, query.DateTo, cancellationToken);
|
||||||
|
var counts = DashboardRegions.Count(workOrders);
|
||||||
|
return new DashboardRegionResponseDTO
|
||||||
|
{
|
||||||
|
Items = DashboardRegions.Names
|
||||||
|
.Select(region => new DashboardRegionRowDTO
|
||||||
|
{
|
||||||
|
Region = region,
|
||||||
|
WorkOrderCount = counts[region]
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,10 @@ namespace SeaHaven.Services.Interfaces
|
||||||
DashboardStatsQueryDTO query,
|
DashboardStatsQueryDTO query,
|
||||||
int page,
|
int page,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<DashboardRegionResponseDTO> GetRegionsAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
DashboardStatsQueryDTO query,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue