mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-02 02:43:22 +00:00
fix(dashboard): count unassigned legacy rows with no lifecycle status (SH-392)
The legacy create paths (WorkOrderDTOs, SyncService, the Blazor WorkorderService) still write Status without LifecycleStatus. The board status filter only matched LifecycleStatus. So an open unassigned row of that kind was left out of the Unassigned tile and its drill-down list, even though the Open tile in the same response counted it. ApplyStatusFilter now reads a row with no LifecycleStatus by its legacy status (LegacyStatus ?? Status), using the Phase0 backfill rules: known text maps as LifecycleStatusMapper does, and anything else, blank included, counts as Incomplete. The tile and /board/search share the predicate, so the count still matches the list it opens.
This commit is contained in:
parent
c49a98db18
commit
d6c5357fab
3 changed files with 165 additions and 3 deletions
|
|
@ -54,9 +54,11 @@ public class DashboardUnassignedTests
|
|||
int id,
|
||||
DateTime? scheduled,
|
||||
string? assignTo = null,
|
||||
LifecycleStatus status = LifecycleStatus.Scheduled,
|
||||
LifecycleStatus? status = LifecycleStatus.Scheduled,
|
||||
DateOnly? targetWeek = null,
|
||||
int accountId = 1)
|
||||
int accountId = 1,
|
||||
string? legacyStatus = null,
|
||||
string? statusText = null)
|
||||
=> new()
|
||||
{
|
||||
Id = id,
|
||||
|
|
@ -67,9 +69,25 @@ public class DashboardUnassignedTests
|
|||
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
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
private static void Seed(ApplicationDbContext ctx)
|
||||
{
|
||||
ctx.workOrders.AddRange(
|
||||
|
|
@ -111,6 +129,53 @@ public class DashboardUnassignedTests
|
|||
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 and 15.
|
||||
inRange.Unassigned.Should().Be(6);
|
||||
allTime.Unassigned.Should().Be(8);
|
||||
}
|
||||
|
||||
[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<IEnumerable<int>> Ids(LifecycleStatus status)
|
||||
=> (await NewSearch(ctx).SearchAsync(new WorkOrderAdvancedSearchQueryDto
|
||||
{
|
||||
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
|
||||
DateFrom = From,
|
||||
DateTo = To,
|
||||
Statuses = new List<LifecycleStatus> { 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()
|
||||
{
|
||||
|
|
@ -152,6 +217,7 @@ public class DashboardUnassignedTests
|
|||
{
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -163,6 +163,44 @@ namespace SeaHaven.DataServices.Helpers
|
|||
|| !WorkOrderBoardRegions.KnownStorageValues.Contains(w.Locations.State))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lower-cased legacy status text and the lifecycle status it stands for:
|
||||
/// the Phase0 backfill rules, kept equal to LifecycleStatusMapper.FromLegacyStatus.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyDictionary<string, LifecycleStatus> LegacyStatusValues =
|
||||
new Dictionary<string, LifecycleStatus>(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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Keeps rows in <paramref name="statuses"/>. 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
|
||||
/// <see cref="LegacyStatusValues"/>, anything else, blank included, is Incomplete.
|
||||
/// </summary>
|
||||
public static IQueryable<WorkOrder> ApplyStatusFilter(
|
||||
IQueryable<WorkOrder> query,
|
||||
IReadOnlyList<LifecycleStatus>? statuses)
|
||||
|
|
@ -170,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<WorkOrder> ApplyDocStatusFilter(
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue