diff --git a/Api.SeaHavenIndustries.Tests/DashboardControllerTests.cs b/Api.SeaHavenIndustries.Tests/DashboardControllerTests.cs
index 4ef3b58..ddd6082 100644
--- a/Api.SeaHavenIndustries.Tests/DashboardControllerTests.cs
+++ b/Api.SeaHavenIndustries.Tests/DashboardControllerTests.cs
@@ -40,7 +40,8 @@ public class DashboardControllerTests
{
ScheduledTomorrow = 3,
PendingUplifts = 5,
- AvetaPending = 7
+ AvetaPending = 7,
+ Unassigned = 11
});
var result = await NewController(service).GetStats(
@@ -50,5 +51,6 @@ public class DashboardControllerTests
((int)Prop(body, "scheduledTomorrow")).Should().Be(3);
((int)Prop(body, "pendingUplifts")).Should().Be(5);
((int)Prop(body, "avetaPending")).Should().Be(7);
+ ((int)Prop(body, "unassigned")).Should().Be(11);
}
}
diff --git a/Api.SeaHavenIndustries.Tests/DashboardUnassignedTests.cs b/Api.SeaHavenIndustries.Tests/DashboardUnassignedTests.cs
new file mode 100644
index 0000000..996b53c
--- /dev/null
+++ b/Api.SeaHavenIndustries.Tests/DashboardUnassignedTests.cs
@@ -0,0 +1,249 @@
+using System.Security.Claims;
+using Data.SeaHavenIndustries;
+using Data.SeaHavenIndustries.Enums;
+using FluentAssertions;
+using Microsoft.EntityFrameworkCore;
+using SeaHaven.DataServices.Implementation;
+using SeaHaven.Services.DTOs;
+using SeaHaven.Services.Helpers;
+using SeaHaven.Services.Implementation;
+using Xunit;
+
+namespace Api.SeaHavenIndustries.Tests;
+
+///
+/// The Dashboard "Unassigned" count is open work orders with no
+/// dispatcher in the selected period, and it equals the Work Orders list the
+/// tile drills into (GET /board/search, Dispatcher = Unassigned).
+///
+public class DashboardUnassignedTests
+{
+ private static readonly DateOnly From = new(2026, 9, 21);
+ private static readonly DateOnly To = new(2026, 9, 25);
+
+ private static readonly List OpenStatuses = Enum.GetValues()
+ .Where(s => s != LifecycleStatus.Completed && s != LifecycleStatus.Canceled)
+ .ToList();
+
+ private static ApplicationDbContext NewContext()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
+ .Options;
+ return new ApplicationDbContext(options);
+ }
+
+ private static WorkOrderAccountResolver Resolver(ApplicationDbContext ctx)
+ => new(new AccountDataService(ctx), new LocationDataService(ctx));
+
+ private static DashboardService NewDashboard(ApplicationDbContext ctx)
+ => new(new DashboardDataService(ctx), Resolver(ctx));
+
+ private static WorkOrderAdvancedSearchService NewSearch(ApplicationDbContext ctx)
+ => new(new WorkOrderAdvancedSearchDataService(ctx), Resolver(ctx));
+
+ private static ClaimsPrincipal AccountUser(int accountId, string role = "Admin", string userId = "admin-1")
+ => new(new ClaimsIdentity(new[]
+ {
+ new Claim(SeaHavenClaimTypes.AccountId, accountId.ToString()),
+ new Claim(ClaimTypes.NameIdentifier, userId),
+ new Claim(ClaimTypes.Role, role)
+ }, "test"));
+
+ private static WorkOrder Wo(
+ int id,
+ DateTime? scheduled,
+ string? assignTo = null,
+ LifecycleStatus? status = LifecycleStatus.Scheduled,
+ DateOnly? targetWeek = null,
+ int accountId = 1,
+ string? legacyStatus = null,
+ string? statusText = null)
+ => new()
+ {
+ Id = id,
+ AccountId = accountId,
+ InternalWONumber = $"2000000{id:0000}",
+ AssignTo = assignTo,
+ ScheduledDate = scheduled,
+ ScheduleWeekOnly = targetWeek.HasValue,
+ TargetWeek = targetWeek,
+ LifecycleStatus = status,
+ LegacyStatus = legacyStatus,
+ Status = statusText,
+ istemplate = false
+ };
+
+ // Rows written by legacy create paths, which set Status but no LifecycleStatus.
+ private static void SeedLegacy(ApplicationDbContext ctx)
+ {
+ var inRange = new DateTime(2026, 9, 22);
+ ctx.workOrders.AddRange(
+ Wo(13, inRange, status: null, statusText: "Open"), // counted
+ Wo(14, inRange, status: null, statusText: "On Hold"), // counted: Pending
+ Wo(15, inRange, status: null), // counted: no status at all
+ Wo(16, inRange, status: null, legacyStatus: "Done", statusText: "Open"), // closed: LegacyStatus wins
+ Wo(17, inRange, status: null, statusText: " Cancelled "), // canceled
+ Wo(18, inRange, assignTo: "disp-1", status: null, statusText: "Open"), // assigned
+ Wo(19, inRange, status: null, legacyStatus: "Open", statusText: "Done")); // counted: LegacyStatus wins
+ ctx.SaveChanges();
+ }
+
+ private static void Seed(ApplicationDbContext ctx)
+ {
+ ctx.workOrders.AddRange(
+ Wo(1, new DateTime(2026, 9, 22)), // counted
+ Wo(2, new DateTime(2026, 9, 23), assignTo: ""), // counted: blank assignee
+ Wo(3, null, targetWeek: new DateOnly(2026, 9, 21)), // counted: week-only in range
+ Wo(4, new DateTime(2026, 9, 24), assignTo: "disp-1"), // assigned
+ Wo(5, new DateTime(2026, 9, 24), status: LifecycleStatus.Completed),
+ Wo(6, new DateTime(2026, 9, 24), status: LifecycleStatus.Canceled),
+ Wo(7, new DateTime(2026, 8, 4)), // out of range
+ Wo(8, null, status: LifecycleStatus.Incomplete), // undated
+ Wo(9, new DateTime(2026, 9, 22), accountId: 2), // other tenant
+ Wo(10, null, status: LifecycleStatus.Incomplete, accountId: 2)); // other tenant, undated
+ ctx.SaveChanges();
+ }
+
+ [Fact]
+ public async Task GetStatsAsync_Range_CountsOpenUnassignedScheduledInPeriod()
+ {
+ using var ctx = NewContext();
+ Seed(ctx);
+
+ var stats = await NewDashboard(ctx).GetStatsAsync(
+ AccountUser(1), new DashboardStatsQueryDTO { DateFrom = From, DateTo = To }, CancellationToken.None);
+
+ stats.Unassigned.Should().Be(3);
+ }
+
+ [Fact]
+ public async Task GetStatsAsync_AllTime_CountsEveryOpenUnassignedIncludingUndated()
+ {
+ using var ctx = NewContext();
+ Seed(ctx);
+
+ var stats = await NewDashboard(ctx).GetStatsAsync(
+ AccountUser(1), new DashboardStatsQueryDTO(), CancellationToken.None);
+
+ // 1, 2, 3, 7 (out of any week but open) and 8 (undated).
+ stats.Unassigned.Should().Be(5);
+ }
+
+ [Fact]
+ public async Task GetStatsAsync_CountsOpenLegacyRowsWithNoLifecycleStatus()
+ {
+ using var ctx = NewContext();
+ Seed(ctx);
+ SeedLegacy(ctx);
+
+ var inRange = await NewDashboard(ctx).GetStatsAsync(
+ AccountUser(1), new DashboardStatsQueryDTO { DateFrom = From, DateTo = To }, CancellationToken.None);
+ var allTime = await NewDashboard(ctx).GetStatsAsync(
+ AccountUser(1), new DashboardStatsQueryDTO(), CancellationToken.None);
+
+ // 1, 2, 3 plus legacy 13, 14, 15 and 19.
+ inRange.Unassigned.Should().Be(7);
+ allTime.Unassigned.Should().Be(9);
+ }
+
+ [Fact]
+ public async Task BoardSearch_StatusFilter_MatchesLegacyRowsByTheirLegacyStatus()
+ {
+ using var ctx = NewContext();
+ var inRange = new DateTime(2026, 9, 22);
+ ctx.workOrders.AddRange(
+ Wo(1, inRange),
+ Wo(2, inRange, status: null, statusText: "scheduled"),
+ Wo(3, inRange, status: null, statusText: "Open"),
+ Wo(4, inRange, status: null, statusText: "Legacy Mystery"),
+ Wo(5, inRange, status: null));
+ ctx.SaveChanges();
+ var user = AccountUser(1);
+
+ async Task> Ids(LifecycleStatus status)
+ => (await NewSearch(ctx).SearchAsync(new WorkOrderAdvancedSearchQueryDto
+ {
+ DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
+ DateFrom = From,
+ DateTo = To,
+ Statuses = new List { status },
+ PageSize = 200
+ }, user, "admin-1")).Items.Select(row => row.Id);
+
+ (await Ids(LifecycleStatus.Scheduled)).Should().BeEquivalentTo(new[] { 1, 2 });
+ // Unknown or missing legacy text reads as Incomplete, as the Phase0 backfill set it.
+ (await Ids(LifecycleStatus.Incomplete)).Should().BeEquivalentTo(new[] { 3, 4, 5 });
+ (await Ids(LifecycleStatus.Completed)).Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task GetStatsAsync_Unassigned_NeverCountsAnotherTenantsWorkOrders()
+ {
+ using var ctx = NewContext();
+ Seed(ctx);
+
+ var accountTwo = await NewDashboard(ctx).GetStatsAsync(
+ AccountUser(2), new DashboardStatsQueryDTO(), CancellationToken.None);
+ var accountTwoInRange = await NewDashboard(ctx).GetStatsAsync(
+ AccountUser(2), new DashboardStatsQueryDTO { DateFrom = From, DateTo = To }, CancellationToken.None);
+
+ accountTwo.Unassigned.Should().Be(2);
+ accountTwoInRange.Unassigned.Should().Be(1);
+ }
+
+ [Fact]
+ public async Task GetStatsAsync_DispatcherScope_HasNoUnassignedWork()
+ {
+ using var ctx = NewContext();
+ Seed(ctx);
+
+ var dispatcher = await NewDashboard(ctx).GetStatsAsync(
+ AccountUser(1, role: "Dispatcher", userId: "disp-1"),
+ new DashboardStatsQueryDTO(),
+ CancellationToken.None);
+ var adminPickingDispatcher = await NewDashboard(ctx).GetStatsAsync(
+ AccountUser(1),
+ new DashboardStatsQueryDTO { DispatcherId = "disp-1" },
+ CancellationToken.None);
+
+ dispatcher.Unassigned.Should().Be(0);
+ adminPickingDispatcher.Unassigned.Should().Be(0);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task GetStatsAsync_UnassignedMatchesTheDrillDownList(bool withRange)
+ {
+ using var ctx = NewContext();
+ Seed(ctx);
+ SeedLegacy(ctx);
+ // Dated outside the board's all-weeks window: neither side may count them.
+ ctx.workOrders.AddRange(Wo(11, new DateTime(1999, 12, 31)), Wo(12, new DateTime(2100, 1, 1)));
+ ctx.SaveChanges();
+ var user = AccountUser(1);
+
+ var stats = await NewDashboard(ctx).GetStatsAsync(
+ user,
+ withRange ? new DashboardStatsQueryDTO { DateFrom = From, DateTo = To } : new DashboardStatsQueryDTO(),
+ CancellationToken.None);
+
+ // What the Work Orders board sends for the tile's drill-down link.
+ var list = await NewSearch(ctx).SearchAsync(new WorkOrderAdvancedSearchQueryDto
+ {
+ DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
+ DateFrom = withRange ? From : new DateOnly(2000, 1, 1),
+ DateTo = withRange ? To : new DateOnly(2099, 12, 31),
+ IncludeDateless = !withRange,
+ Dispatchers = new List { "__unassigned__" },
+ Statuses = OpenStatuses,
+ PageSize = 200
+ }, user, "admin-1");
+
+ stats.Unassigned.Should().Be(list.TotalCount);
+ var listed = list.Items.Select(row => row.Id).ToList();
+ listed.Should().Contain(new[] { 13, 14, 15, 19 });
+ listed.Should().NotContain(new[] { 16, 17, 18 });
+ }
+}
diff --git a/Api.SeaHavenIndustries/Controllers/DashboardController.cs b/Api.SeaHavenIndustries/Controllers/DashboardController.cs
index b53c4bf..e40c9fd 100644
--- a/Api.SeaHavenIndustries/Controllers/DashboardController.cs
+++ b/Api.SeaHavenIndustries/Controllers/DashboardController.cs
@@ -33,6 +33,7 @@ namespace Api.SeaHavenIndustries.Controllers
scheduledTomorrow = stats.ScheduledTomorrow,
pendingUplifts = stats.PendingUplifts,
avetaPending = stats.AvetaPending,
+ unassigned = stats.Unassigned,
breakdown = stats.Breakdown,
dueCount = stats.DueCount,
completedDueCount = stats.CompletedDueCount,
diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs
index b665f2c..583aaa2 100644
--- a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs
+++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs
@@ -5,6 +5,14 @@ namespace SeaHaven.DataServices.Helpers
{
public static class WorkOrderBoardQueryFilters
{
+ ///
+ /// The window the Work Orders board searches when no date range is selected
+ /// (client ADVANCED_SEARCH_ALL_WEEKS_FROM/TO), used with includeDateless.
+ /// Counts that drill into that list use it too, so both see the same rows.
+ ///
+ public static readonly DateOnly AllWeeksFrom = new(2000, 1, 1);
+ public static readonly DateOnly AllWeeksTo = new(2099, 12, 31);
+
public static IQueryable ApplyBaseScope(IQueryable query)
=> query.Where(w => w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null));
@@ -155,6 +163,44 @@ namespace SeaHaven.DataServices.Helpers
|| !WorkOrderBoardRegions.KnownStorageValues.Contains(w.Locations.State))));
}
+ ///
+ /// Lower-cased legacy status text and the lifecycle status it stands for:
+ /// the Phase0 backfill rules, kept equal to LifecycleStatusMapper.FromLegacyStatus.
+ ///
+ public static readonly IReadOnlyDictionary LegacyStatusValues =
+ new Dictionary(StringComparer.Ordinal)
+ {
+ ["open"] = LifecycleStatus.Incomplete,
+ ["incomplete"] = LifecycleStatus.Incomplete,
+ ["pending"] = LifecycleStatus.Pending,
+ ["onhold"] = LifecycleStatus.Pending,
+ ["on hold"] = LifecycleStatus.Pending,
+ ["scheduled"] = LifecycleStatus.Scheduled,
+ ["en route"] = LifecycleStatus.EnRoute,
+ ["enroute"] = LifecycleStatus.EnRoute,
+ ["on site"] = LifecycleStatus.OnSite,
+ ["onsite"] = LifecycleStatus.OnSite,
+ ["inprogress"] = LifecycleStatus.InProgress,
+ ["in progress"] = LifecycleStatus.InProgress,
+ ["completed"] = LifecycleStatus.Completed,
+ ["complete"] = LifecycleStatus.Completed,
+ ["done"] = LifecycleStatus.Completed,
+ ["closed"] = LifecycleStatus.Completed,
+ ["rescheduled"] = LifecycleStatus.Rescheduled,
+ ["cancelled"] = LifecycleStatus.Canceled,
+ ["canceled"] = LifecycleStatus.Canceled,
+ ["pending quote"] = LifecycleStatus.PendingQuote,
+ ["pendingquote"] = LifecycleStatus.PendingQuote,
+ ["pendingapproval"] = LifecycleStatus.PendingQuote,
+ ["pending approval"] = LifecycleStatus.PendingQuote,
+ };
+
+ ///
+ /// Keeps rows in . Rows with no LifecycleStatus
+ /// (legacy create paths still write only Status) match by their legacy
+ /// status, read as the Phase0 backfill does: known text maps through
+ /// , anything else, blank included, is Incomplete.
+ ///
public static IQueryable ApplyStatusFilter(
IQueryable query,
IReadOnlyList? statuses)
@@ -162,7 +208,22 @@ namespace SeaHaven.DataServices.Helpers
if (statuses == null || statuses.Count == 0)
return query;
- return query.Where(w => w.LifecycleStatus != null && statuses.Contains(w.LifecycleStatus.Value));
+ var legacyValues = LegacyStatusValues
+ .Where(entry => statuses.Contains(entry.Value))
+ .Select(entry => entry.Key)
+ .ToList();
+ var knownLegacyValues = LegacyStatusValues.Keys.ToList();
+ var includeUnknownLegacy = statuses.Contains(LifecycleStatus.Incomplete);
+
+ // Null check before Trim(): EF translates it, the in-memory provider runs it.
+ return query.Where(w =>
+ (w.LifecycleStatus != null && statuses.Contains(w.LifecycleStatus.Value))
+ || (w.LifecycleStatus == null
+ && (((w.LegacyStatus ?? w.Status) != null
+ && legacyValues.Contains((w.LegacyStatus ?? w.Status)!.Trim().ToLower()))
+ || (includeUnknownLegacy
+ && ((w.LegacyStatus ?? w.Status) == null
+ || !knownLegacyValues.Contains((w.LegacyStatus ?? w.Status)!.Trim().ToLower()))))));
}
public static IQueryable ApplyDocStatusFilter(
diff --git a/SeaHaven.DataServices/Implementation/DashboardDataService.cs b/SeaHaven.DataServices/Implementation/DashboardDataService.cs
index a6c03f2..33957fc 100644
--- a/SeaHaven.DataServices/Implementation/DashboardDataService.cs
+++ b/SeaHaven.DataServices/Implementation/DashboardDataService.cs
@@ -39,6 +39,41 @@ namespace SeaHaven.DataServices.Implementation
};
}
+ private static readonly IReadOnlyList OpenLifecycleStatuses = Enum
+ .GetValues()
+ .Where(s => s != LifecycleStatus.Completed && s != LifecycleStatus.Canceled)
+ .ToList();
+
+ public async Task CountOpenUnassignedAsync(
+ int? accountId,
+ string? dispatcherId,
+ DateOnly? dateFrom,
+ DateOnly? dateTo,
+ CancellationToken cancellationToken)
+ {
+ // A dispatcher-scoped Dashboard has no unassigned work by definition.
+ if (dispatcherId != null)
+ return 0;
+
+ var query = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
+ if (accountId is int scopedAccountId)
+ query = WorkOrderBoardQueryFilters.ApplyAccountScope(query, scopedAccountId);
+ query = WorkOrderBoardQueryFilters.ApplyDispatcherFilter(
+ query, new[] { "__unassigned__" }, myWorkOrders: false, currentUserId: null);
+ query = WorkOrderBoardQueryFilters.ApplyStatusFilter(query, OpenLifecycleStatuses);
+ // All time counts what the board lists with no range: every week plus
+ // undated work, so the tile and its drill-down agree.
+ query = dateFrom is DateOnly from && dateTo is DateOnly to
+ ? WorkOrderBoardQueryFilters.ApplyDateRangeFilter(query, from, to)
+ : WorkOrderBoardQueryFilters.ApplyDateRangeFilter(
+ query,
+ WorkOrderBoardQueryFilters.AllWeeksFrom,
+ WorkOrderBoardQueryFilters.AllWeeksTo,
+ includeDateless: true);
+
+ return await query.CountAsync(cancellationToken);
+ }
+
public async Task> GetDashboardWorkOrdersAsync(
int? accountId,
string? dispatcherId,
diff --git a/SeaHaven.DataServices/Interfaces/IDashboardDataService.cs b/SeaHaven.DataServices/Interfaces/IDashboardDataService.cs
index 51ef7b5..ee1932d 100644
--- a/SeaHaven.DataServices/Interfaces/IDashboardDataService.cs
+++ b/SeaHaven.DataServices/Interfaces/IDashboardDataService.cs
@@ -14,6 +14,18 @@ namespace SeaHaven.DataServices.Interfaces
DateOnly today,
CancellationToken cancellationToken);
+ ///
+ /// Open work orders with no dispatcher in the period, counted with the
+ /// same filters as GET /board/search for Dispatcher=Unassigned,
+ /// so the Dashboard number matches the list it drills into.
+ ///
+ Task CountOpenUnassignedAsync(
+ int? accountId,
+ string? dispatcherId,
+ DateOnly? dateFrom,
+ DateOnly? dateTo,
+ CancellationToken cancellationToken);
+
Task> GetDashboardWorkOrdersAsync(
int? accountId,
string? dispatcherId,
diff --git a/SeaHaven.Services/DTOs/DashboardDTOs.cs b/SeaHaven.Services/DTOs/DashboardDTOs.cs
index 9cf76c0..7e4438f 100644
--- a/SeaHaven.Services/DTOs/DashboardDTOs.cs
+++ b/SeaHaven.Services/DTOs/DashboardDTOs.cs
@@ -106,6 +106,8 @@ namespace SeaHaven.Services.DTOs
public int ScheduledTomorrow { get; set; }
public int PendingUplifts { get; set; }
public int AvetaPending { get; set; }
+ /// Open work orders with no dispatcher in the selected period.
+ public int Unassigned { get; set; }
public DashboardBreakdownDTO Breakdown { get; set; } = new();
public int DueCount { get; set; }
public int CompletedDueCount { get; set; }
diff --git a/SeaHaven.Services/Implementation/DashboardService.cs b/SeaHaven.Services/Implementation/DashboardService.cs
index 02642dc..e528b97 100644
--- a/SeaHaven.Services/Implementation/DashboardService.cs
+++ b/SeaHaven.Services/Implementation/DashboardService.cs
@@ -43,6 +43,12 @@ namespace SeaHaven.Services.Implementation
query.DateTo,
cancellationToken);
var metrics = DashboardMetrics.Calculate(workOrders, DashboardBusinessTime.Today());
+ var unassigned = await _dataService.CountOpenUnassignedAsync(
+ accountId,
+ dispatcherId,
+ query.DateFrom,
+ query.DateTo,
+ cancellationToken);
var useMetrics = query.DateFrom.HasValue && query.DateTo.HasValue || dispatcherId is not null;
@@ -55,6 +61,7 @@ namespace SeaHaven.Services.Implementation
ScheduledTomorrow = kpis.ScheduledTomorrow,
PendingUplifts = kpis.PendingUplifts,
AvetaPending = kpis.AvetaPending,
+ Unassigned = unassigned,
Breakdown = metrics.Breakdown,
DueCount = metrics.DueCount,
CompletedDueCount = metrics.CompletedDueCount,
diff --git a/SeaHavenIndustries.Tests/LifecycleStatusMapperTests.cs b/SeaHavenIndustries.Tests/LifecycleStatusMapperTests.cs
index 0a06141..5e0149f 100644
--- a/SeaHavenIndustries.Tests/LifecycleStatusMapperTests.cs
+++ b/SeaHavenIndustries.Tests/LifecycleStatusMapperTests.cs
@@ -1,4 +1,5 @@
using Data.SeaHavenIndustries.Enums;
+using SeaHaven.DataServices.Helpers;
using SeaHaven.Services.Constants;
using SeaHaven.Services.Helpers;
@@ -30,6 +31,48 @@ public class LifecycleStatusMapperTests
Assert.Null(result);
}
+ // Every value FromLegacyStatus recognizes: the board's status filter reads
+ // rows with no LifecycleStatus through its own copy of this table.
+ [Theory]
+ [InlineData("open")]
+ [InlineData("incomplete")]
+ [InlineData("pending")]
+ [InlineData("onhold")]
+ [InlineData("on hold")]
+ [InlineData("scheduled")]
+ [InlineData("en route")]
+ [InlineData("enroute")]
+ [InlineData("on site")]
+ [InlineData("onsite")]
+ [InlineData("inprogress")]
+ [InlineData("in progress")]
+ [InlineData("completed")]
+ [InlineData("complete")]
+ [InlineData("done")]
+ [InlineData("closed")]
+ [InlineData("rescheduled")]
+ [InlineData("cancelled")]
+ [InlineData("canceled")]
+ [InlineData("pending quote")]
+ [InlineData("pendingquote")]
+ [InlineData("pendingapproval")]
+ [InlineData("pending approval")]
+ public void BoardStatusFilterLegacyTable_AgreesWithFromLegacyStatus(string legacy)
+ {
+ Assert.Equal(
+ LifecycleStatusMapper.FromLegacyStatus(legacy),
+ WorkOrderBoardQueryFilters.LegacyStatusValues[legacy]);
+ }
+
+ [Fact]
+ public void BoardStatusFilterLegacyTable_HasNoValueTheMapperRejects()
+ {
+ Assert.Equal(23, WorkOrderBoardQueryFilters.LegacyStatusValues.Count);
+ Assert.All(
+ WorkOrderBoardQueryFilters.LegacyStatusValues,
+ entry => Assert.Equal(entry.Value, LifecycleStatusMapper.FromLegacyStatus(entry.Key)));
+ }
+
[Theory]
[InlineData(LifecycleStatus.EnRoute, "En Route")]
[InlineData(LifecycleStatus.PendingQuote, "Pending Quote")]