feat(work-orders): add region filtering (SH-348)

This commit is contained in:
Alexandre Brandizzi 2026-09-16 21:27:05 -03:00
parent 3358ea5058
commit 738c9eaf45
8 changed files with 133 additions and 60 deletions

View file

@ -121,6 +121,40 @@ namespace SeaHaven.DataServices.Helpers
return query.Where(w => w.SiteCode != null && normalized.Contains(w.SiteCode));
}
public static IQueryable<WorkOrder> ApplyRegionFilter(
IQueryable<WorkOrder> query,
IReadOnlyList<string>? 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<WorkOrder> ApplyStatusFilter(
IQueryable<WorkOrder> query,
IReadOnlyList<LifecycleStatus>? statuses)

View file

@ -0,0 +1,45 @@
namespace SeaHaven.DataServices.Helpers
{
public static class WorkOrderBoardRegions
{
public static readonly IReadOnlyList<string> Names =
["East", "Central", "West", "California", "Unmapped/Other"];
private static readonly IReadOnlyDictionary<string, string[]> RegionStateCodes =
new Dictionary<string, string[]>(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<string, string> StateNames =
new Dictionary<string, string>(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<string> StorageValuesFor(string region)
=> RegionStateCodes.TryGetValue(region, out var codes)
? codes.SelectMany(code => new[] { code, StateNames[code] }).ToArray()
: Array.Empty<string>();
public static IReadOnlySet<string> KnownStorageValues { get; } =
RegionStateCodes.Values
.SelectMany(codes => codes.SelectMany(code => new[] { code, StateNames[code] }))
.ToHashSet(StringComparer.Ordinal);
}
}

View file

@ -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);

View file

@ -9,6 +9,7 @@ namespace SeaHaven.DataServices.Interfaces
DateOnly DateTo,
bool UnscheduledOnly,
IReadOnlyList<string>? Sites,
IReadOnlyList<string>? Regions,
IReadOnlyList<WorkOrderType>? Types,
bool Overdue,
IReadOnlyList<string>? Dispatchers,

View file

@ -73,6 +73,7 @@ namespace SeaHaven.Services.DTOs
/// </summary>
public bool UnscheduledOnly { get; set; }
public List<string>? Sites { get; set; }
public List<string>? Regions { get; set; }
public List<WorkOrderType>? Types { get; set; }
/// <summary>When true, include past-due (isPastDue) rows. Combined with Types via OR.</summary>
public bool Overdue { get; set; }

View file

@ -1,20 +1,25 @@
using SeaHaven.DataServices.Helpers;
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 readonly IReadOnlyList<string> 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<string, int> Count(
@ -26,59 +31,4 @@ public static class DashboardRegions
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"
};
}

View file

@ -49,6 +49,7 @@ namespace SeaHaven.Services.Implementation
dateRange.To,
query.UnscheduledOnly,
query.Sites,
query.Regions,
query.Types,
query.Overdue,
query.Dispatchers,

View file

@ -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<string> { "West" };
var west = await service.SearchAsync(query, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
Assert.Equal(new[] { 1 }, west.Items.Select(item => item.Id));
query.Regions = new List<string> { "California" };
var california = await service.SearchAsync(query, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
Assert.Equal(new[] { 2 }, california.Items.Select(item => item.Id));
query.Regions = new List<string> { "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()
{