mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
Merge branch 'dev' into feat/ab/sh-210-uplift-decisions
This commit is contained in:
commit
ef371b5917
6 changed files with 347 additions and 34 deletions
|
|
@ -270,11 +270,13 @@ public class DashboardServiceTests
|
|||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
result.TotalDispatchers.Should().Be(1);
|
||||
result.Items.Should().ContainSingle();
|
||||
result.Items[0].DispatcherName.Should().Be("Ada One");
|
||||
result.TotalDispatchers.Should().Be(2);
|
||||
result.Items.Select(item => item.DispatcherName)
|
||||
.Should().ContainInOrder("Ada One", "Ben Two");
|
||||
result.Items[0].TotalCount.Should().Be(2);
|
||||
result.Items[0].OpenCount.Should().Be(1);
|
||||
result.Items[1].TotalCount.Should().Be(0);
|
||||
result.Items[1].OpenCount.Should().Be(0);
|
||||
result.PageSize.Should().Be(10);
|
||||
|
||||
var adminResult = await NewService(ctx).GetWorkloadAsync(
|
||||
|
|
@ -444,6 +446,275 @@ public class DashboardServiceTests
|
|||
result.Items[0].RescheduleRate.Should().Be(50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatsAsync_DispatcherQuerySelectionCannotBroadenScope()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
ctx.workOrders.AddRange(
|
||||
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-1" },
|
||||
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-1" },
|
||||
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-2" },
|
||||
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-2" },
|
||||
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-2" });
|
||||
ctx.SaveChanges();
|
||||
|
||||
var query = new DashboardStatsQueryDTO { DispatcherId = "dispatcher-2" };
|
||||
var stats = await NewService(ctx).GetStatsAsync(
|
||||
AccountUser(1, "dispatcher-1", "Dispatcher"), query, CancellationToken.None);
|
||||
|
||||
stats.Total.Should().Be(2);
|
||||
stats.Open.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatsAsync_AdminCanSelectIndividualDispatcherOrMine()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
ctx.workOrders.AddRange(
|
||||
new WorkOrder { AccountId = 1, Status = "Done", AssignTo = "admin-1" },
|
||||
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-2" },
|
||||
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-2" },
|
||||
new WorkOrder { AccountId = 1, Status = "Open", AssignTo = "dispatcher-2" });
|
||||
ctx.SaveChanges();
|
||||
|
||||
var service = NewService(ctx);
|
||||
|
||||
var selected = await service.GetStatsAsync(
|
||||
AccountUser(1, "admin-1"),
|
||||
new DashboardStatsQueryDTO { DispatcherId = "dispatcher-2" },
|
||||
CancellationToken.None);
|
||||
selected.Total.Should().Be(3);
|
||||
selected.Open.Should().Be(3);
|
||||
|
||||
var mine = await service.GetStatsAsync(
|
||||
AccountUser(1, "admin-1"),
|
||||
new DashboardStatsQueryDTO { DispatcherId = "mine" },
|
||||
CancellationToken.None);
|
||||
mine.Total.Should().Be(1);
|
||||
mine.Completed.Should().Be(1);
|
||||
|
||||
var all = await service.GetStatsAsync(
|
||||
AccountUser(1, "admin-1"),
|
||||
new DashboardStatsQueryDTO(),
|
||||
CancellationToken.None);
|
||||
all.Total.Should().Be(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatsAsync_ManagerSelectionScopesStats()
|
||||
{
|
||||
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, "manager-1", "Manager"),
|
||||
new DashboardStatsQueryDTO { DispatcherId = "dispatcher-2" },
|
||||
CancellationToken.None);
|
||||
|
||||
stats.Total.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatsAsync_SchedulerSelectionScopesStats()
|
||||
{
|
||||
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, "scheduler-1", "Scheduler"),
|
||||
new DashboardStatsQueryDTO { DispatcherId = "dispatcher-2" },
|
||||
CancellationToken.None);
|
||||
|
||||
stats.Total.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDashboardAsync_UnauthorizedRoleFailsClosed()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
var service = NewService(ctx);
|
||||
var user = AccountUser(1, "tech-1", "Technician");
|
||||
|
||||
var statsAct = () => service.GetStatsAsync(
|
||||
user, new DashboardStatsQueryDTO(), CancellationToken.None);
|
||||
var statsException = await statsAct.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
||||
statsException.Which.Code.Should().Be("Forbidden");
|
||||
|
||||
var workloadAct = () => service.GetWorkloadAsync(
|
||||
user, new DashboardStatsQueryDTO(), 1, CancellationToken.None);
|
||||
var workloadException = await workloadAct.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
||||
workloadException.Which.Code.Should().Be("Forbidden");
|
||||
|
||||
var performanceAct = () => service.GetPerformanceAsync(
|
||||
user, new DashboardStatsQueryDTO(), 1, CancellationToken.None);
|
||||
var performanceException = await performanceAct.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
||||
performanceException.Which.Code.Should().Be("Forbidden");
|
||||
|
||||
var regionsAct = () => service.GetRegionsAsync(
|
||||
user, new DashboardStatsQueryDTO(), CancellationToken.None);
|
||||
var regionsException = await regionsAct.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
||||
regionsException.Which.Code.Should().Be("Forbidden");
|
||||
|
||||
var trendAct = () => service.GetTrendAsync(
|
||||
user, new DashboardTrendQueryDTO(), CancellationToken.None);
|
||||
var trendException = await trendAct.Should().ThrowAsync<WorkOrderBoardValidationException>();
|
||||
trendException.Which.Code.Should().Be("Forbidden");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetWorkloadAsync_IndividualSelectionPreservesRosterWithZeroRows()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
ctx.Roles.Add(new IdentityRole
|
||||
{
|
||||
Id = "dispatcher-role",
|
||||
Name = "Dispatcher",
|
||||
NormalizedName = "DISPATCHER"
|
||||
});
|
||||
ctx.Users.AddRange(
|
||||
new ApplicationUser { Id = "dispatcher-1", FirstName = "Ada", LastName = "One" },
|
||||
new ApplicationUser { Id = "dispatcher-2", FirstName = "Ben", LastName = "Two" });
|
||||
ctx.UserRoles.AddRange(
|
||||
new IdentityUserRole<string> { UserId = "dispatcher-1", RoleId = "dispatcher-role" },
|
||||
new IdentityUserRole<string> { UserId = "dispatcher-2", RoleId = "dispatcher-role" });
|
||||
ctx.workOrders.AddRange(
|
||||
new WorkOrder { AccountId = 1, AssignTo = "dispatcher-1", Status = "Open" },
|
||||
new WorkOrder { AccountId = 1, AssignTo = "dispatcher-1", Status = "Open" });
|
||||
ctx.SaveChanges();
|
||||
|
||||
var workload = await NewService(ctx).GetWorkloadAsync(
|
||||
AccountUser(1, "admin-1"),
|
||||
new DashboardStatsQueryDTO { DispatcherId = "dispatcher-1" },
|
||||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
workload.TotalDispatchers.Should().Be(2);
|
||||
workload.Items.Should().HaveCount(2);
|
||||
workload.Items[0].DispatcherId.Should().Be("dispatcher-1");
|
||||
workload.Items[0].TotalCount.Should().Be(2);
|
||||
workload.Items[0].OpenCount.Should().Be(2);
|
||||
workload.Items[1].DispatcherId.Should().Be("dispatcher-2");
|
||||
workload.Items[1].TotalCount.Should().Be(0);
|
||||
workload.Items[1].OpenCount.Should().Be(0);
|
||||
workload.Page.Should().Be(1);
|
||||
workload.PageSize.Should().Be(10);
|
||||
|
||||
var performance = await NewService(ctx).GetPerformanceAsync(
|
||||
AccountUser(1, "admin-1"),
|
||||
new DashboardStatsQueryDTO { DispatcherId = "dispatcher-1" },
|
||||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
performance.TotalDispatchers.Should().Be(2);
|
||||
performance.Items.Should().HaveCount(2);
|
||||
performance.Items[0].DispatcherId.Should().Be("dispatcher-1");
|
||||
performance.Items[0].AssignedCount.Should().Be(2);
|
||||
performance.Items[1].DispatcherId.Should().Be("dispatcher-2");
|
||||
performance.Items[1].AssignedCount.Should().Be(0);
|
||||
performance.Items[1].CompletedCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPerformanceAsync_PopulatesAssignedAndCompletedCounts()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
ctx.Roles.Add(new IdentityRole
|
||||
{
|
||||
Id = "dispatcher-role",
|
||||
Name = "Dispatcher",
|
||||
NormalizedName = "DISPATCHER"
|
||||
});
|
||||
ctx.Users.Add(new ApplicationUser { Id = "dispatcher-1", FirstName = "Ada", LastName = "One" });
|
||||
ctx.UserRoles.Add(new IdentityUserRole<string>
|
||||
{
|
||||
UserId = "dispatcher-1",
|
||||
RoleId = "dispatcher-role"
|
||||
});
|
||||
ctx.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
AccountId = 1,
|
||||
AssignTo = "dispatcher-1",
|
||||
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Completed
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
AccountId = 1,
|
||||
AssignTo = "dispatcher-1",
|
||||
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Scheduled
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
AccountId = 1,
|
||||
AssignTo = "dispatcher-1",
|
||||
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Canceled
|
||||
});
|
||||
ctx.SaveChanges();
|
||||
|
||||
var result = await NewService(ctx).GetPerformanceAsync(
|
||||
AccountUser(1),
|
||||
new DashboardStatsQueryDTO(),
|
||||
1,
|
||||
CancellationToken.None);
|
||||
|
||||
result.Items.Should().ContainSingle();
|
||||
result.Items[0].AssignedCount.Should().Be(3);
|
||||
result.Items[0].CompletedCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatsAsync_StatusDistributionBucketsResolvedStatuses()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
ctx.workOrders.AddRange(
|
||||
new WorkOrder
|
||||
{
|
||||
AccountId = 1,
|
||||
LifecycleStatus = Data.SeaHavenIndustries.Enums.LifecycleStatus.Scheduled
|
||||
},
|
||||
new WorkOrder { AccountId = 1, Status = "In Progress" },
|
||||
new WorkOrder { AccountId = 1, Status = "Done" },
|
||||
new WorkOrder { AccountId = 1, Status = "Done" },
|
||||
new WorkOrder { AccountId = 1, Status = "Unparseable" });
|
||||
ctx.SaveChanges();
|
||||
|
||||
var stats = await NewService(ctx).GetStatsAsync(
|
||||
AccountUser(1, "admin-1"),
|
||||
new DashboardStatsQueryDTO(),
|
||||
CancellationToken.None);
|
||||
|
||||
stats.StatusDistribution.Should().HaveCount(4);
|
||||
stats.StatusDistribution
|
||||
.Select(bucket => (bucket.Status, bucket.Count))
|
||||
.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
(Status: "Completed", Count: 2),
|
||||
(Status: "In Progress", Count: 1),
|
||||
(Status: "Scheduled", Count: 1),
|
||||
(Status: "Unknown", Count: 1)
|
||||
});
|
||||
stats.StatusDistribution.Sum(bucket => bucket.Count).Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatsAsync_StatusDistributionIsEmptyWithoutWorkOrders()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
|
||||
var stats = await NewService(ctx).GetStatsAsync(
|
||||
AccountUser(1, "admin-1"),
|
||||
new DashboardStatsQueryDTO(),
|
||||
CancellationToken.None);
|
||||
|
||||
stats.StatusDistribution.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTrendAsync_DailyBucketsClassifyByCalendarScheduledDate()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
dueCount = stats.DueCount,
|
||||
completedDueCount = stats.CompletedDueCount,
|
||||
completionRate = stats.CompletionRate,
|
||||
averageResolutionDays = stats.AverageResolutionDays
|
||||
averageResolutionDays = stats.AverageResolutionDays,
|
||||
statusDistribution = stats.StatusDistribution
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -166,7 +166,6 @@ namespace SeaHaven.DataServices.Implementation
|
|||
on userRole.RoleId equals role.Id
|
||||
where user.IsDeleted != true
|
||||
&& (role.Name == "Dispatcher" || role.Name == "DISPATCHER")
|
||||
&& (dispatcherId == null || user.Id == dispatcherId)
|
||||
select new
|
||||
{
|
||||
user.Id,
|
||||
|
|
@ -178,6 +177,9 @@ namespace SeaHaven.DataServices.Implementation
|
|||
if (accountId is int scopedAccountId)
|
||||
workOrders = WorkOrderBoardQueryFilters.ApplyAccountScope(workOrders, scopedAccountId);
|
||||
|
||||
if (dispatcherId != null)
|
||||
workOrders = workOrders.Where(w => w.AssignTo == dispatcherId);
|
||||
|
||||
if (dateFrom is DateOnly from && dateTo is DateOnly to)
|
||||
{
|
||||
var fromDate = from.ToDateTime(TimeOnly.MinValue);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace SeaHaven.Services.DTOs
|
|||
{
|
||||
public DateOnly? DateFrom { get; set; }
|
||||
public DateOnly? DateTo { get; set; }
|
||||
public string? DispatcherId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DashboardTrendQueryDTO : DashboardStatsQueryDTO
|
||||
|
|
@ -63,6 +64,8 @@ namespace SeaHaven.Services.DTOs
|
|||
public string? Color { get; init; }
|
||||
public decimal CompletionRate { get; init; }
|
||||
public decimal? AverageResolutionDays { get; init; }
|
||||
public int AssignedCount { get; init; }
|
||||
public int CompletedCount { get; init; }
|
||||
}
|
||||
|
||||
public sealed class DashboardRegionResponseDTO
|
||||
|
|
@ -108,6 +111,13 @@ namespace SeaHaven.Services.DTOs
|
|||
public int CompletedDueCount { get; set; }
|
||||
public decimal CompletionRate { get; set; }
|
||||
public decimal? AverageResolutionDays { get; set; }
|
||||
public IReadOnlyList<DashboardStatusBucketDTO> StatusDistribution { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class DashboardStatusBucketDTO
|
||||
{
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public int Count { get; init; }
|
||||
}
|
||||
|
||||
public sealed class DashboardBreakdownDTO
|
||||
|
|
|
|||
|
|
@ -61,9 +61,26 @@ public static class DashboardMetrics
|
|||
dueCount,
|
||||
completedDueCount,
|
||||
dueCount == 0 ? 0 : Math.Round((decimal)completedDueCount / dueCount * 100, 2),
|
||||
resolutionDays.Count == 0 ? null : Math.Round(resolutionDays.Average(), 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,
|
||||
|
|
@ -110,4 +127,5 @@ public sealed record DashboardMetricResult(
|
|||
int DueCount,
|
||||
int CompletedDueCount,
|
||||
decimal CompletionRate,
|
||||
decimal? AverageResolutionDays);
|
||||
decimal? AverageResolutionDays,
|
||||
IReadOnlyList<DashboardStatusBucketDTO> StatusDistribution);
|
||||
|
|
|
|||
|
|
@ -28,11 +28,7 @@ namespace SeaHaven.Services.Implementation
|
|||
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 dispatcherId = ResolveDispatcherScope(user, query);
|
||||
|
||||
var counts = await _dataService.GetWorkOrderCountsAsync(accountId, cancellationToken);
|
||||
var kpis = await _dataService.GetKpiCountsAsync(
|
||||
|
|
@ -63,7 +59,8 @@ namespace SeaHaven.Services.Implementation
|
|||
DueCount = metrics.DueCount,
|
||||
CompletedDueCount = metrics.CompletedDueCount,
|
||||
CompletionRate = metrics.CompletionRate,
|
||||
AverageResolutionDays = metrics.AverageResolutionDays
|
||||
AverageResolutionDays = metrics.AverageResolutionDays,
|
||||
StatusDistribution = metrics.StatusDistribution
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -77,11 +74,7 @@ namespace SeaHaven.Services.Implementation
|
|||
throw new ArgumentOutOfRangeException(nameof(page));
|
||||
|
||||
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 dispatcherId = ResolveDispatcherScope(user, query);
|
||||
|
||||
var rows = await _dataService.GetDispatcherWorkloadAsync(
|
||||
accountId,
|
||||
|
|
@ -123,11 +116,7 @@ namespace SeaHaven.Services.Implementation
|
|||
throw new ArgumentOutOfRangeException(nameof(page));
|
||||
|
||||
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 dispatcherId = ResolveDispatcherScope(user, query);
|
||||
|
||||
var directory = await _dataService.GetDispatcherWorkloadAsync(
|
||||
accountId, dispatcherId, query.DateFrom, query.DateTo, cancellationToken);
|
||||
|
|
@ -145,7 +134,9 @@ namespace SeaHaven.Services.Implementation
|
|||
DispatcherName = dispatcher.DispatcherName,
|
||||
Color = dispatcher.Color,
|
||||
CompletionRate = metrics.CompletionRate,
|
||||
AverageResolutionDays = metrics.AverageResolutionDays
|
||||
AverageResolutionDays = metrics.AverageResolutionDays,
|
||||
AssignedCount = metrics.Total,
|
||||
CompletedCount = metrics.Completed
|
||||
};
|
||||
})
|
||||
.OrderByDescending(row => row.CompletionRate)
|
||||
|
|
@ -169,11 +160,7 @@ namespace SeaHaven.Services.Implementation
|
|||
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 dispatcherId = ResolveDispatcherScope(user, query);
|
||||
|
||||
var workOrders = await _dataService.GetDashboardWorkOrdersAsync(
|
||||
accountId, dispatcherId, query.DateFrom, query.DateTo, cancellationToken);
|
||||
|
|
@ -196,11 +183,7 @@ namespace SeaHaven.Services.Implementation
|
|||
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 dispatcherId = ResolveDispatcherScope(user, query);
|
||||
|
||||
var today = DashboardBusinessTime.Today();
|
||||
var (windowFrom, windowTo, granularity) = ResolveTrendWindow(query, today);
|
||||
|
|
@ -404,5 +387,33 @@ namespace SeaHaven.Services.Implementation
|
|||
TotalVendors = rows.Count
|
||||
};
|
||||
}
|
||||
|
||||
private string? ResolveDispatcherScope(ClaimsPrincipal user, DashboardStatsQueryDTO query)
|
||||
{
|
||||
if (user.IsInRole("Dispatcher"))
|
||||
{
|
||||
var dispatcherId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrWhiteSpace(dispatcherId))
|
||||
throw new WorkOrderBoardValidationException("Forbidden", "Dispatcher identity is required.");
|
||||
return dispatcherId;
|
||||
}
|
||||
|
||||
if (user.IsInRole("Admin") || user.IsInRole("Manager") || user.IsInRole("Scheduler"))
|
||||
{
|
||||
var selected = query.DispatcherId;
|
||||
if (string.IsNullOrWhiteSpace(selected))
|
||||
return null;
|
||||
if (selected.Equals("mine", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var identity = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrWhiteSpace(identity))
|
||||
throw new WorkOrderBoardValidationException("Forbidden", "Dispatcher identity is required.");
|
||||
return identity;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
throw new WorkOrderBoardValidationException("Forbidden", "Dashboard access requires an authorized role.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue