From 564bd70301d57b100291223c6157dadcb6c35118 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 17 Sep 2026 03:37:36 -0300 Subject: [PATCH] fix(dashboard): scope Trend by picker and admit Scheduler to dispatcher picker ResolveDispatcherScope now admits the Scheduler role, which owns the viewAllDispatchersOnDashboard permission, so it can load Stats, Workload, Performance, Regions and Trend instead of being denied. GetTrendAsync now resolves scope via the shared ResolveDispatcherScope so a picker DispatcherId selection scopes Trend consistently with the other endpoints and unauthorized roles fail closed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../DashboardServiceTests.cs | 22 +++++++++++++++++++ .../Implementation/DashboardService.cs | 8 ++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs b/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs index b1bb099..2cc73ee 100644 --- a/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs +++ b/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs @@ -517,6 +517,23 @@ public class DashboardServiceTests 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() { @@ -543,6 +560,11 @@ public class DashboardServiceTests user, new DashboardStatsQueryDTO(), CancellationToken.None); var regionsException = await regionsAct.Should().ThrowAsync(); regionsException.Which.Code.Should().Be("Forbidden"); + + var trendAct = () => service.GetTrendAsync( + user, new DashboardTrendQueryDTO(), CancellationToken.None); + var trendException = await trendAct.Should().ThrowAsync(); + trendException.Which.Code.Should().Be("Forbidden"); } [Fact] diff --git a/SeaHaven.Services/Implementation/DashboardService.cs b/SeaHaven.Services/Implementation/DashboardService.cs index 68c4a5f..02642dc 100644 --- a/SeaHaven.Services/Implementation/DashboardService.cs +++ b/SeaHaven.Services/Implementation/DashboardService.cs @@ -183,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); @@ -402,7 +398,7 @@ namespace SeaHaven.Services.Implementation return dispatcherId; } - if (user.IsInRole("Admin") || user.IsInRole("Manager")) + if (user.IsInRole("Admin") || user.IsInRole("Manager") || user.IsInRole("Scheduler")) { var selected = query.DispatcherId; if (string.IsNullOrWhiteSpace(selected))