From 738c9eaf4503aa79732fbd0aa035efd79cbed3e6 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Wed, 16 Sep 2026 21:27:05 -0300 Subject: [PATCH 1/2] feat(work-orders): add region filtering (SH-348) --- .../Helpers/WorkOrderBoardQueryFilters.cs | 34 +++++++++ .../Helpers/WorkOrderBoardRegions.cs | 45 ++++++++++++ .../WorkOrderAdvancedSearchDataService.cs | 1 + .../WorkOrderAdvancedSearchModels.cs | 1 + .../DTOs/WorkOrderBoardRequestDTOs.cs | 1 + SeaHaven.Services/Helpers/DashboardRegions.cs | 70 +++---------------- .../WorkOrderAdvancedSearchService.cs | 1 + .../WorkOrderBoardSearchTests.cs | 40 +++++++++++ 8 files changed, 133 insertions(+), 60 deletions(-) create mode 100644 SeaHaven.DataServices/Helpers/WorkOrderBoardRegions.cs diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs index 7ff26ba..71c4d94 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs @@ -121,6 +121,40 @@ namespace SeaHaven.DataServices.Helpers return query.Where(w => w.SiteCode != null && normalized.Contains(w.SiteCode)); } + public static IQueryable ApplyRegionFilter( + IQueryable query, + IReadOnlyList? regions) + { + if (regions == null || regions.Count == 0) + return query; + + var normalized = regions + .Where(region => !string.IsNullOrWhiteSpace(region)) + .Select(region => region.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + if (normalized.Count == 0) + return query; + + var knownRegions = normalized + .Where(region => WorkOrderBoardRegions.Names.Contains(region) && region != "Unmapped/Other") + .SelectMany(WorkOrderBoardRegions.StorageValuesFor) + .Distinct(StringComparer.Ordinal) + .ToList(); + var includeUnmapped = normalized.Contains("Unmapped/Other", StringComparer.Ordinal); + var hasKnownRegion = knownRegions.Count > 0; + + if (!hasKnownRegion && !includeUnmapped) + return query.Where(_ => false); + + return query.Where(w => + (hasKnownRegion && w.Locations != null && w.Locations.State != null && knownRegions.Contains(w.Locations.State)) + || (includeUnmapped && (w.Locations == null + || w.Locations.State == null + || w.Locations.State == "" + || !WorkOrderBoardRegions.KnownStorageValues.Contains(w.Locations.State)))); + } + public static IQueryable ApplyStatusFilter( IQueryable query, IReadOnlyList? statuses) diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardRegions.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardRegions.cs new file mode 100644 index 0000000..0ca39ec --- /dev/null +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardRegions.cs @@ -0,0 +1,45 @@ +namespace SeaHaven.DataServices.Helpers +{ + public static class WorkOrderBoardRegions + { + public static readonly IReadOnlyList Names = + ["East", "Central", "West", "California", "Unmapped/Other"]; + + private static readonly IReadOnlyDictionary RegionStateCodes = + new Dictionary(StringComparer.Ordinal) + { + ["East"] = ["CT", "DE", "GA", "ME", "MD", "MA", "NH", "NJ", "NY", "NC", "PA", "RI", "SC", "VT", "VA", "WV"], + ["Central"] = ["AL", "AR", "FL", "IL", "IN", "IA", "KS", "KY", "LA", "MI", "MN", "MS", "MO", "NE", "ND", "OH", "OK", "SD", "TN", "WI"], + ["West"] = ["AK", "AZ", "CO", "HI", "ID", "MT", "NV", "NM", "OR", "TX", "UT", "WA", "WY"], + ["California"] = ["CA"] + }; + + private static readonly IReadOnlyDictionary StateNames = + new Dictionary(StringComparer.Ordinal) + { + ["AL"] = "ALABAMA", ["AK"] = "ALASKA", ["AZ"] = "ARIZONA", ["AR"] = "ARKANSAS", + ["CA"] = "CALIFORNIA", ["CO"] = "COLORADO", ["CT"] = "CONNECTICUT", ["DE"] = "DELAWARE", + ["FL"] = "FLORIDA", ["GA"] = "GEORGIA", ["HI"] = "HAWAII", ["ID"] = "IDAHO", + ["IL"] = "ILLINOIS", ["IN"] = "INDIANA", ["IA"] = "IOWA", ["KS"] = "KANSAS", + ["KY"] = "KENTUCKY", ["LA"] = "LOUISIANA", ["ME"] = "MAINE", ["MD"] = "MARYLAND", + ["MA"] = "MASSACHUSETTS", ["MI"] = "MICHIGAN", ["MN"] = "MINNESOTA", ["MS"] = "MISSISSIPPI", + ["MO"] = "MISSOURI", ["MT"] = "MONTANA", ["NE"] = "NEBRASKA", ["NV"] = "NEVADA", + ["NH"] = "NEW HAMPSHIRE", ["NJ"] = "NEW JERSEY", ["NM"] = "NEW MEXICO", ["NY"] = "NEW YORK", + ["NC"] = "NORTH CAROLINA", ["ND"] = "NORTH DAKOTA", ["OH"] = "OHIO", ["OK"] = "OKLAHOMA", + ["OR"] = "OREGON", ["PA"] = "PENNSYLVANIA", ["RI"] = "RHODE ISLAND", ["SC"] = "SOUTH CAROLINA", + ["SD"] = "SOUTH DAKOTA", ["TN"] = "TENNESSEE", ["TX"] = "TEXAS", ["UT"] = "UTAH", + ["VT"] = "VERMONT", ["VA"] = "VIRGINIA", ["WA"] = "WASHINGTON", ["WV"] = "WEST VIRGINIA", + ["WI"] = "WISCONSIN", ["WY"] = "WYOMING" + }; + + public static IReadOnlyCollection StorageValuesFor(string region) + => RegionStateCodes.TryGetValue(region, out var codes) + ? codes.SelectMany(code => new[] { code, StateNames[code] }).ToArray() + : Array.Empty(); + + public static IReadOnlySet KnownStorageValues { get; } = + RegionStateCodes.Values + .SelectMany(codes => codes.SelectMany(code => new[] { code, StateNames[code] })) + .ToHashSet(StringComparer.Ordinal); + } +} diff --git a/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs b/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs index ceac856..eeb28c5 100644 --- a/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs +++ b/SeaHaven.DataServices/Implementation/WorkOrderAdvancedSearchDataService.cs @@ -24,6 +24,7 @@ namespace SeaHaven.DataServices.Implementation else baseQuery = WorkOrderBoardQueryFilters.ApplyDateRangeFilter(baseQuery, query.DateFrom, query.DateTo); baseQuery = WorkOrderBoardQueryFilters.ApplySiteFilter(baseQuery, query.Sites); + baseQuery = WorkOrderBoardQueryFilters.ApplyRegionFilter(baseQuery, query.Regions); baseQuery = WorkOrderBoardQueryFilters.ApplyTypeFilter(baseQuery, query.Types, query.Overdue); baseQuery = WorkOrderBoardQueryFilters.ApplyDispatcherFilter( baseQuery, query.Dispatchers, query.MyWorkOrders, query.CurrentUserId); diff --git a/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs b/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs index 576af7a..2c0a627 100644 --- a/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs +++ b/SeaHaven.DataServices/Interfaces/WorkOrderAdvancedSearchModels.cs @@ -9,6 +9,7 @@ namespace SeaHaven.DataServices.Interfaces DateOnly DateTo, bool UnscheduledOnly, IReadOnlyList? Sites, + IReadOnlyList? Regions, IReadOnlyList? Types, bool Overdue, IReadOnlyList? Dispatchers, diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs index 8c04373..b90380c 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs @@ -73,6 +73,7 @@ namespace SeaHaven.Services.DTOs /// public bool UnscheduledOnly { get; set; } public List? Sites { get; set; } + public List? Regions { get; set; } public List? Types { get; set; } /// When true, include past-due (isPastDue) rows. Combined with Types via OR. public bool Overdue { get; set; } diff --git a/SeaHaven.Services/Helpers/DashboardRegions.cs b/SeaHaven.Services/Helpers/DashboardRegions.cs index f3b5fae..45f23aa 100644 --- a/SeaHaven.Services/Helpers/DashboardRegions.cs +++ b/SeaHaven.Services/Helpers/DashboardRegions.cs @@ -1,20 +1,25 @@ +using SeaHaven.DataServices.Helpers; using SeaHaven.DataServices.Interfaces; namespace SeaHaven.Services.Helpers; public static class DashboardRegions { - public static readonly IReadOnlyList Names = - ["East", "Central", "West", "California", "Unmapped/Other"]; + public static readonly IReadOnlyList Names = WorkOrderBoardRegions.Names; 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"; + var normalized = state.Trim().ToUpperInvariant(); + foreach (var region in Names.Where(region => region != "Unmapped/Other")) + { + if (WorkOrderBoardRegions.StorageValuesFor(region).Contains(normalized)) + return region; + } + + return "Unmapped/Other"; } public static IReadOnlyDictionary Count( @@ -26,59 +31,4 @@ public static class DashboardRegions return counts; } - - private static readonly IReadOnlyDictionary StateToRegion = - new Dictionary(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" - }; } diff --git a/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs b/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs index 6c4e3da..fa97dc5 100644 --- a/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderAdvancedSearchService.cs @@ -49,6 +49,7 @@ namespace SeaHaven.Services.Implementation dateRange.To, query.UnscheduledOnly, query.Sites, + query.Regions, query.Types, query.Overdue, query.Dispatchers, diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs index 721291a..033d3fb 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs @@ -228,6 +228,46 @@ public class WorkOrderAdvancedSearchServiceTests Assert.Equal("BK5", result.Items.First().SiteCode); } + [Fact] + public async Task SearchAsync_FiltersByCanonicalRegionAndUnmappedBucket() + { + await using var context = CreateContext(); + var date = new DateTime(2026, 6, 23); + context.Locations.AddRange( + new Locations { Id = 1, State = "WA" }, + new Locations { Id = 2, State = "CALIFORNIA" }, + new Locations { Id = 3, State = "Atlantis" }, + new Locations { Id = 4, State = null }); + context.workOrders.AddRange( + new WorkOrder { Id = 1, LocationId = 1, ScheduledDate = date, LifecycleStatus = LifecycleStatus.Scheduled }, + new WorkOrder { Id = 2, LocationId = 2, ScheduledDate = date, LifecycleStatus = LifecycleStatus.Scheduled }, + new WorkOrder { Id = 3, LocationId = 3, ScheduledDate = date, LifecycleStatus = LifecycleStatus.Scheduled }, + new WorkOrder { Id = 4, LocationId = 4, ScheduledDate = date, LifecycleStatus = LifecycleStatus.Scheduled }); + await context.SaveChangesAsync(); + + var dataService = new WorkOrderAdvancedSearchDataService(context); + var service = new SeaHaven.Services.Implementation.WorkOrderAdvancedSearchService( + dataService, WorkOrderAccountTestHelpers.Resolver(context)); + var query = new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto + { + DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom, + DateFrom = new DateOnly(2026, 6, 22), DateTo = new DateOnly(2026, 6, 30), + PageSize = 50 + }; + + query.Regions = new List { "West" }; + var west = await service.SearchAsync(query, WorkOrderAccountTestHelpers.OrgWideAdmin(), null); + Assert.Equal(new[] { 1 }, west.Items.Select(item => item.Id)); + + query.Regions = new List { "California" }; + var california = await service.SearchAsync(query, WorkOrderAccountTestHelpers.OrgWideAdmin(), null); + Assert.Equal(new[] { 2 }, california.Items.Select(item => item.Id)); + + query.Regions = new List { "Unmapped/Other" }; + var unmapped = await service.SearchAsync(query, WorkOrderAccountTestHelpers.OrgWideAdmin(), null); + Assert.Equal(new[] { 3, 4 }, unmapped.Items.Select(item => item.Id)); + } + [Fact] public async Task SearchAsync_ClampsPageSizeToMax() { From 146fe6fdb016f844989e76f827d596fa6c9c905e Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 17 Sep 2026 02:26:19 -0300 Subject: [PATCH 2/2] style: satisfy dotnet format whitespace gate for region board files Break collection initializer entries onto one line each in WorkOrderBoardRegions and WorkOrderBoardSearchTests so the changed-file formatting gate (dotnet format --verify-no-changes) passes. --- .../Helpers/WorkOrderBoardRegions.cs | 63 +++++++++++++++---- .../WorkOrderBoardSearchTests.cs | 3 +- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardRegions.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardRegions.cs index 0ca39ec..17a2d5e 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardRegions.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardRegions.cs @@ -17,19 +17,56 @@ namespace SeaHaven.DataServices.Helpers private static readonly IReadOnlyDictionary StateNames = new Dictionary(StringComparer.Ordinal) { - ["AL"] = "ALABAMA", ["AK"] = "ALASKA", ["AZ"] = "ARIZONA", ["AR"] = "ARKANSAS", - ["CA"] = "CALIFORNIA", ["CO"] = "COLORADO", ["CT"] = "CONNECTICUT", ["DE"] = "DELAWARE", - ["FL"] = "FLORIDA", ["GA"] = "GEORGIA", ["HI"] = "HAWAII", ["ID"] = "IDAHO", - ["IL"] = "ILLINOIS", ["IN"] = "INDIANA", ["IA"] = "IOWA", ["KS"] = "KANSAS", - ["KY"] = "KENTUCKY", ["LA"] = "LOUISIANA", ["ME"] = "MAINE", ["MD"] = "MARYLAND", - ["MA"] = "MASSACHUSETTS", ["MI"] = "MICHIGAN", ["MN"] = "MINNESOTA", ["MS"] = "MISSISSIPPI", - ["MO"] = "MISSOURI", ["MT"] = "MONTANA", ["NE"] = "NEBRASKA", ["NV"] = "NEVADA", - ["NH"] = "NEW HAMPSHIRE", ["NJ"] = "NEW JERSEY", ["NM"] = "NEW MEXICO", ["NY"] = "NEW YORK", - ["NC"] = "NORTH CAROLINA", ["ND"] = "NORTH DAKOTA", ["OH"] = "OHIO", ["OK"] = "OKLAHOMA", - ["OR"] = "OREGON", ["PA"] = "PENNSYLVANIA", ["RI"] = "RHODE ISLAND", ["SC"] = "SOUTH CAROLINA", - ["SD"] = "SOUTH DAKOTA", ["TN"] = "TENNESSEE", ["TX"] = "TEXAS", ["UT"] = "UTAH", - ["VT"] = "VERMONT", ["VA"] = "VIRGINIA", ["WA"] = "WASHINGTON", ["WV"] = "WEST VIRGINIA", - ["WI"] = "WISCONSIN", ["WY"] = "WYOMING" + ["AL"] = "ALABAMA", + ["AK"] = "ALASKA", + ["AZ"] = "ARIZONA", + ["AR"] = "ARKANSAS", + ["CA"] = "CALIFORNIA", + ["CO"] = "COLORADO", + ["CT"] = "CONNECTICUT", + ["DE"] = "DELAWARE", + ["FL"] = "FLORIDA", + ["GA"] = "GEORGIA", + ["HI"] = "HAWAII", + ["ID"] = "IDAHO", + ["IL"] = "ILLINOIS", + ["IN"] = "INDIANA", + ["IA"] = "IOWA", + ["KS"] = "KANSAS", + ["KY"] = "KENTUCKY", + ["LA"] = "LOUISIANA", + ["ME"] = "MAINE", + ["MD"] = "MARYLAND", + ["MA"] = "MASSACHUSETTS", + ["MI"] = "MICHIGAN", + ["MN"] = "MINNESOTA", + ["MS"] = "MISSISSIPPI", + ["MO"] = "MISSOURI", + ["MT"] = "MONTANA", + ["NE"] = "NEBRASKA", + ["NV"] = "NEVADA", + ["NH"] = "NEW HAMPSHIRE", + ["NJ"] = "NEW JERSEY", + ["NM"] = "NEW MEXICO", + ["NY"] = "NEW YORK", + ["NC"] = "NORTH CAROLINA", + ["ND"] = "NORTH DAKOTA", + ["OH"] = "OHIO", + ["OK"] = "OKLAHOMA", + ["OR"] = "OREGON", + ["PA"] = "PENNSYLVANIA", + ["RI"] = "RHODE ISLAND", + ["SC"] = "SOUTH CAROLINA", + ["SD"] = "SOUTH DAKOTA", + ["TN"] = "TENNESSEE", + ["TX"] = "TEXAS", + ["UT"] = "UTAH", + ["VT"] = "VERMONT", + ["VA"] = "VIRGINIA", + ["WA"] = "WASHINGTON", + ["WV"] = "WEST VIRGINIA", + ["WI"] = "WISCONSIN", + ["WY"] = "WYOMING" }; public static IReadOnlyCollection StorageValuesFor(string region) diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs index 033d3fb..cd47e05 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs @@ -251,7 +251,8 @@ public class WorkOrderAdvancedSearchServiceTests var query = new SeaHaven.Services.DTOs.WorkOrderAdvancedSearchQueryDto { DatePreset = SeaHaven.Services.Helpers.WorkOrderAdvancedSearchDatePreset.Custom, - DateFrom = new DateOnly(2026, 6, 22), DateTo = new DateOnly(2026, 6, 30), + DateFrom = new DateOnly(2026, 6, 22), + DateTo = new DateOnly(2026, 6, 30), PageSize = 50 };