fix(work-orders): align phase 2 status/type contract to SHOC frontend

Expand LifecycleStatus (EnRoute, OnSite, Rescheduled, Pending, PendingQuote) and WorkOrderType (Reactive, AddOn) to match the frontend prototype. Add FE label round-trip via LifecycleStatusMapper/WorkOrderTypeMapper, update derived fields, mutation rules and board unscheduled filter, and reactivate [Authorize] on WorkOrderController. Fix pre-existing dayGroup test expectation.
This commit is contained in:
Arthur Bassi 2026-07-07 13:11:22 -03:00
parent d040832b87
commit 673bc3b5a9
12 changed files with 158 additions and 64 deletions

View file

@ -22,7 +22,7 @@ using SeaHaven.DataServices.Interfaces;
namespace Api.SeaHavenIndustries.Controllers namespace Api.SeaHavenIndustries.Controllers
{ {
// [Authorize] [Authorize]
[ApiController] [ApiController]
[Route("api/WorkOrder")] [Route("api/WorkOrder")]
[Route("api/workorders")] [Route("api/workorders")]

View file

@ -10,21 +10,22 @@ namespace Data.SeaHavenIndustries.Enums
OnHold = 5 OnHold = 5
} }
/// <summary>10 operational lifecycle values aligned with SHOC board.</summary> /// <summary>10 operational lifecycle values aligned with SHOC board (FE labels).</summary>
public enum LifecycleStatus public enum LifecycleStatus
{ {
Incomplete = 1, Incomplete = 1,
Scheduled = 2, Pending = 2,
InProgress = 3, Scheduled = 3,
Complete = 4, EnRoute = 4,
Canceled = 5, OnSite = 5,
OnHold = 6, InProgress = 6,
PendingVendor = 7, Completed = 7,
PendingApproval = 8, Rescheduled = 8,
NeedsReview = 9, Canceled = 9,
Closed = 10 PendingQuote = 10
} }
/// <summary>Board work-order types. Legacy values retained for backward compatibility.</summary>
public enum WorkOrderType public enum WorkOrderType
{ {
PO = 1, PO = 1,
@ -32,6 +33,8 @@ namespace Data.SeaHavenIndustries.Enums
Emergency = 3, Emergency = 3,
Project = 4, Project = 4,
Inspection = 5, Inspection = 5,
Reactive = 6,
AddOn = 7,
Other = 99 Other = 99
} }

View file

@ -35,9 +35,8 @@ namespace SeaHaven.DataServices.Implementation
var unscheduledBase = baseQuery.Where(w => var unscheduledBase = baseQuery.Where(w =>
w.ScheduledDate == null w.ScheduledDate == null
&& w.LifecycleStatus != LifecycleStatus.Complete && w.LifecycleStatus != LifecycleStatus.Completed
&& w.LifecycleStatus != LifecycleStatus.Canceled && w.LifecycleStatus != LifecycleStatus.Canceled);
&& w.LifecycleStatus != LifecycleStatus.Closed);
var scheduledTotal = await scheduledBase.CountAsync(); var scheduledTotal = await scheduledBase.CountAsync();

View file

@ -10,25 +10,76 @@ namespace SeaHaven.Services.Helpers
return status.Trim().ToLowerInvariant() switch return status.Trim().ToLowerInvariant() switch
{ {
"open" => LifecycleStatus.Incomplete, "open" or "incomplete" => LifecycleStatus.Incomplete,
"inprogress" or "in progress" => LifecycleStatus.InProgress, "pending" or "onhold" or "on hold" => LifecycleStatus.Pending,
"completed" or "complete" => LifecycleStatus.Complete,
"cancelled" or "canceled" => LifecycleStatus.Canceled,
"onhold" or "on hold" => LifecycleStatus.OnHold,
"scheduled" => LifecycleStatus.Scheduled, "scheduled" => LifecycleStatus.Scheduled,
"pendingvendor" or "pending vendor" => LifecycleStatus.PendingVendor, "en route" or "enroute" => LifecycleStatus.EnRoute,
"pendingapproval" or "pending approval" => LifecycleStatus.PendingApproval, "on site" or "onsite" => LifecycleStatus.OnSite,
"needsreview" or "needs review" => LifecycleStatus.NeedsReview, "inprogress" or "in progress" => LifecycleStatus.InProgress,
"closed" => LifecycleStatus.Closed, "completed" or "complete" or "done" or "closed" => LifecycleStatus.Completed,
_ => LifecycleStatus.NeedsReview "rescheduled" => LifecycleStatus.Rescheduled,
"cancelled" or "canceled" => LifecycleStatus.Canceled,
"pending quote" or "pendingquote" or "pendingapproval" or "pending approval" => LifecycleStatus.PendingQuote,
_ => null
}; };
} }
public static LifecycleStatus? ParseLifecycleStatus(string? value) public static LifecycleStatus? ParseLifecycleStatus(string? value)
{ {
if (string.IsNullOrWhiteSpace(value)) return null; if (string.IsNullOrWhiteSpace(value)) return null;
if (Enum.TryParse<LifecycleStatus>(value, true, out var parsed)) return parsed;
if (TryParseFeLabel(value, out var fromLabel))
return fromLabel;
if (Enum.TryParse<LifecycleStatus>(value, true, out var parsed))
return parsed;
return FromLegacyStatus(value); return FromLegacyStatus(value);
} }
public static bool TryParseFeLabel(string? label, out LifecycleStatus status)
{
status = default;
if (string.IsNullOrWhiteSpace(label)) return false;
return label.Trim().ToLowerInvariant() switch
{
"incomplete" => Set(LifecycleStatus.Incomplete, out status),
"pending" => Set(LifecycleStatus.Pending, out status),
"scheduled" => Set(LifecycleStatus.Scheduled, out status),
"en route" => Set(LifecycleStatus.EnRoute, out status),
"on site" => Set(LifecycleStatus.OnSite, out status),
"in progress" => Set(LifecycleStatus.InProgress, out status),
"completed" => Set(LifecycleStatus.Completed, out status),
"rescheduled" => Set(LifecycleStatus.Rescheduled, out status),
"canceled" => Set(LifecycleStatus.Canceled, out status),
"pending quote" => Set(LifecycleStatus.PendingQuote, out status),
_ => false
};
}
public static string ToFeLabel(LifecycleStatus status) => status switch
{
LifecycleStatus.Incomplete => "Incomplete",
LifecycleStatus.Pending => "Pending",
LifecycleStatus.Scheduled => "Scheduled",
LifecycleStatus.EnRoute => "En Route",
LifecycleStatus.OnSite => "On Site",
LifecycleStatus.InProgress => "In Progress",
LifecycleStatus.Completed => "Completed",
LifecycleStatus.Rescheduled => "Rescheduled",
LifecycleStatus.Canceled => "Canceled",
LifecycleStatus.PendingQuote => "Pending Quote",
_ => status.ToString()
};
public static bool IsTerminal(LifecycleStatus? status)
=> status is LifecycleStatus.Completed or LifecycleStatus.Canceled;
private static bool Set(LifecycleStatus value, out LifecycleStatus status)
{
status = value;
return true;
}
} }
} }

View file

@ -7,8 +7,7 @@ namespace SeaHaven.Services.Helpers
private static readonly HashSet<LifecycleStatus?> ReadOnlyStatuses = new() private static readonly HashSet<LifecycleStatus?> ReadOnlyStatuses = new()
{ {
LifecycleStatus.Canceled, LifecycleStatus.Canceled,
LifecycleStatus.Closed, LifecycleStatus.Completed
LifecycleStatus.Complete
}; };
public static bool IsReadOnly(LifecycleStatus? status) => ReadOnlyStatuses.Contains(status); public static bool IsReadOnly(LifecycleStatus? status) => ReadOnlyStatuses.Contains(status);

View file

@ -4,13 +4,6 @@ namespace SeaHaven.Services.Helpers
{ {
public static class WorkOrderDerivedFields public static class WorkOrderDerivedFields
{ {
private static readonly HashSet<LifecycleStatus?> TerminalStatuses = new()
{
LifecycleStatus.Complete,
LifecycleStatus.Canceled,
LifecycleStatus.Closed
};
public static bool IsPastDue( public static bool IsPastDue(
DateTime? scheduledDate, DateTime? scheduledDate,
LifecycleStatus? lifecycleStatus, LifecycleStatus? lifecycleStatus,
@ -19,7 +12,7 @@ namespace SeaHaven.Services.Helpers
if (!scheduledDate.HasValue) if (!scheduledDate.HasValue)
return false; return false;
if (TerminalStatuses.Contains(lifecycleStatus)) if (LifecycleStatusMapper.IsTerminal(lifecycleStatus))
return false; return false;
var today = (utcNow ?? DateTime.UtcNow).Date; var today = (utcNow ?? DateTime.UtcNow).Date;
@ -27,7 +20,7 @@ namespace SeaHaven.Services.Helpers
} }
public static bool IsTerminal(LifecycleStatus? lifecycleStatus) public static bool IsTerminal(LifecycleStatus? lifecycleStatus)
=> TerminalStatuses.Contains(lifecycleStatus); => LifecycleStatusMapper.IsTerminal(lifecycleStatus);
public static string? GetDayGroup(DateTime? scheduledDate) public static string? GetDayGroup(DateTime? scheduledDate)
{ {
@ -57,24 +50,6 @@ namespace SeaHaven.Services.Helpers
} }
public static string GetLifecycleStatusLabel(LifecycleStatus? status) public static string GetLifecycleStatusLabel(LifecycleStatus? status)
{ => status.HasValue ? LifecycleStatusMapper.ToFeLabel(status.Value) : "";
if (!status.HasValue)
return "";
return status.Value switch
{
LifecycleStatus.Incomplete => "Incomplete",
LifecycleStatus.Scheduled => "Scheduled",
LifecycleStatus.InProgress => "In Progress",
LifecycleStatus.Complete => "Complete",
LifecycleStatus.Canceled => "Canceled",
LifecycleStatus.OnHold => "On Hold",
LifecycleStatus.PendingVendor => "Pending Vendor",
LifecycleStatus.PendingApproval => "Pending Approval",
LifecycleStatus.NeedsReview => "Needs Review",
LifecycleStatus.Closed => "Closed",
_ => status.Value.ToString()
};
}
} }
} }

View file

@ -0,0 +1,47 @@
using Data.SeaHavenIndustries.Enums;
namespace SeaHaven.Services.Helpers
{
public static class WorkOrderTypeMapper
{
/// <summary>Board FE types. Overdue is not a type — use <see cref="WorkOrderDerivedFields.IsPastDue(System.DateTime?, LifecycleStatus?, System.DateTime?)"/>.</summary>
public static bool TryParseFeLabel(string? label, out WorkOrderType type)
{
type = default;
if (string.IsNullOrWhiteSpace(label)) return false;
return label.Trim().ToLowerInvariant() switch
{
"pm" => Set(WorkOrderType.PM, out type),
"reactive" => Set(WorkOrderType.Reactive, out type),
"emergency" => Set(WorkOrderType.Emergency, out type),
"add-on" or "addon" => Set(WorkOrderType.AddOn, out type),
"po" => Set(WorkOrderType.PO, out type),
"project" => Set(WorkOrderType.Project, out type),
"inspection" => Set(WorkOrderType.Inspection, out type),
"other" => Set(WorkOrderType.Other, out type),
"overdue" => false,
_ => Enum.TryParse(label, true, out type)
};
}
public static string ToFeLabel(WorkOrderType type) => type switch
{
WorkOrderType.PM => "PM",
WorkOrderType.Reactive => "Reactive",
WorkOrderType.Emergency => "Emergency",
WorkOrderType.AddOn => "Add-On",
WorkOrderType.PO => "PO",
WorkOrderType.Project => "Project",
WorkOrderType.Inspection => "Inspection",
WorkOrderType.Other => "Other",
_ => type.ToString()
};
private static bool Set(WorkOrderType value, out WorkOrderType type)
{
type = value;
return true;
}
}
}

View file

@ -222,7 +222,7 @@ namespace SeaHaven.Services.Implementation
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
throw new WorkOrderBoardValidationException("InvalidValue", "workOrderType value is required."); throw new WorkOrderBoardValidationException("InvalidValue", "workOrderType value is required.");
if (!Enum.TryParse<WorkOrderType>(value, true, out var parsed)) if (!WorkOrderTypeMapper.TryParseFeLabel(value, out var parsed))
throw new WorkOrderBoardValidationException("InvalidValue", $"Invalid workOrderType: {value}"); throw new WorkOrderBoardValidationException("InvalidValue", $"Invalid workOrderType: {value}");
var old = workOrder.WorkOrderType?.ToString(); var old = workOrder.WorkOrderType?.ToString();

View file

@ -9,9 +9,9 @@ public class LifecycleStatusMapperTests
[InlineData("Open", LifecycleStatus.Incomplete)] [InlineData("Open", LifecycleStatus.Incomplete)]
[InlineData("InProgress", LifecycleStatus.InProgress)] [InlineData("InProgress", LifecycleStatus.InProgress)]
[InlineData("In Progress", LifecycleStatus.InProgress)] [InlineData("In Progress", LifecycleStatus.InProgress)]
[InlineData("Completed", LifecycleStatus.Complete)] [InlineData("Completed", LifecycleStatus.Completed)]
[InlineData("Cancelled", LifecycleStatus.Canceled)] [InlineData("Cancelled", LifecycleStatus.Canceled)]
[InlineData("On Hold", LifecycleStatus.OnHold)] [InlineData("On Hold", LifecycleStatus.Pending)]
[InlineData("Scheduled", LifecycleStatus.Scheduled)] [InlineData("Scheduled", LifecycleStatus.Scheduled)]
public void FromLegacyStatus_MapsKnownValues(string input, LifecycleStatus expected) public void FromLegacyStatus_MapsKnownValues(string input, LifecycleStatus expected)
{ {
@ -19,10 +19,31 @@ public class LifecycleStatusMapperTests
Assert.Equal(expected, result); Assert.Equal(expected, result);
} }
[Theory]
[InlineData("Incomplete", LifecycleStatus.Incomplete)]
[InlineData("En Route", LifecycleStatus.EnRoute)]
[InlineData("On Site", LifecycleStatus.OnSite)]
[InlineData("Rescheduled", LifecycleStatus.Rescheduled)]
[InlineData("Pending Quote", LifecycleStatus.PendingQuote)]
public void ParseLifecycleStatus_MapsFeLabels(string input, LifecycleStatus expected)
{
var result = LifecycleStatusMapper.ParseLifecycleStatus(input);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(LifecycleStatus.EnRoute, "En Route")]
[InlineData(LifecycleStatus.OnSite, "On Site")]
[InlineData(LifecycleStatus.PendingQuote, "Pending Quote")]
public void ToFeLabel_ReturnsFrontendLabels(LifecycleStatus status, string expected)
{
Assert.Equal(expected, LifecycleStatusMapper.ToFeLabel(status));
}
[Fact] [Fact]
public void FromLegacyStatus_Unknown_ReturnsNeedsReview() public void FromLegacyStatus_Unknown_ReturnsNull()
{ {
var result = LifecycleStatusMapper.FromLegacyStatus("MysteryStatus"); var result = LifecycleStatusMapper.FromLegacyStatus("MysteryStatus");
Assert.Equal(LifecycleStatus.NeedsReview, result); Assert.Null(result);
} }
} }

View file

@ -7,8 +7,7 @@ public class WorkOrderBoardMutationRulesTests
{ {
[Theory] [Theory]
[InlineData(LifecycleStatus.Canceled, true)] [InlineData(LifecycleStatus.Canceled, true)]
[InlineData(LifecycleStatus.Closed, true)] [InlineData(LifecycleStatus.Completed, true)]
[InlineData(LifecycleStatus.Complete, true)]
[InlineData(LifecycleStatus.Scheduled, false)] [InlineData(LifecycleStatus.Scheduled, false)]
[InlineData(LifecycleStatus.Incomplete, false)] [InlineData(LifecycleStatus.Incomplete, false)]
public void IsReadOnly_ReflectsTerminalStatuses(LifecycleStatus status, bool expected) public void IsReadOnly_ReflectsTerminalStatuses(LifecycleStatus status, bool expected)

View file

@ -193,7 +193,7 @@ public class WorkOrderBoardServiceTests
Assert.Equal(1, response.Counts.Total); Assert.Equal(1, response.Counts.Total);
Assert.Single(response.Scheduled); Assert.Single(response.Scheduled);
Assert.True(response.Scheduled[0].IsPastDue); Assert.True(response.Scheduled[0].IsPastDue);
Assert.Equal("wednesday", response.Scheduled[0].DayGroup); Assert.Equal("monday", response.Scheduled[0].DayGroup);
} }
[Fact] [Fact]

View file

@ -34,7 +34,7 @@ public class WorkOrderDerivedFieldsTests
{ {
var result = WorkOrderDerivedFields.IsPastDue( var result = WorkOrderDerivedFields.IsPastDue(
Today.AddDays(-3), Today.AddDays(-3),
LifecycleStatus.Complete, LifecycleStatus.Completed,
Today); Today);
Assert.False(result); Assert.False(result);