mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +00:00
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:
parent
d040832b87
commit
673bc3b5a9
12 changed files with 158 additions and 64 deletions
|
|
@ -22,7 +22,7 @@ using SeaHaven.DataServices.Interfaces;
|
|||
|
||||
namespace Api.SeaHavenIndustries.Controllers
|
||||
{
|
||||
// [Authorize]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/WorkOrder")]
|
||||
[Route("api/workorders")]
|
||||
|
|
|
|||
|
|
@ -10,21 +10,22 @@ namespace Data.SeaHavenIndustries.Enums
|
|||
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
|
||||
{
|
||||
Incomplete = 1,
|
||||
Scheduled = 2,
|
||||
InProgress = 3,
|
||||
Complete = 4,
|
||||
Canceled = 5,
|
||||
OnHold = 6,
|
||||
PendingVendor = 7,
|
||||
PendingApproval = 8,
|
||||
NeedsReview = 9,
|
||||
Closed = 10
|
||||
Pending = 2,
|
||||
Scheduled = 3,
|
||||
EnRoute = 4,
|
||||
OnSite = 5,
|
||||
InProgress = 6,
|
||||
Completed = 7,
|
||||
Rescheduled = 8,
|
||||
Canceled = 9,
|
||||
PendingQuote = 10
|
||||
}
|
||||
|
||||
/// <summary>Board work-order types. Legacy values retained for backward compatibility.</summary>
|
||||
public enum WorkOrderType
|
||||
{
|
||||
PO = 1,
|
||||
|
|
@ -32,6 +33,8 @@ namespace Data.SeaHavenIndustries.Enums
|
|||
Emergency = 3,
|
||||
Project = 4,
|
||||
Inspection = 5,
|
||||
Reactive = 6,
|
||||
AddOn = 7,
|
||||
Other = 99
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,9 +35,8 @@ namespace SeaHaven.DataServices.Implementation
|
|||
|
||||
var unscheduledBase = baseQuery.Where(w =>
|
||||
w.ScheduledDate == null
|
||||
&& w.LifecycleStatus != LifecycleStatus.Complete
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled
|
||||
&& w.LifecycleStatus != LifecycleStatus.Closed);
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled);
|
||||
|
||||
var scheduledTotal = await scheduledBase.CountAsync();
|
||||
|
||||
|
|
|
|||
|
|
@ -10,25 +10,76 @@ namespace SeaHaven.Services.Helpers
|
|||
|
||||
return status.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"open" => LifecycleStatus.Incomplete,
|
||||
"inprogress" or "in progress" => LifecycleStatus.InProgress,
|
||||
"completed" or "complete" => LifecycleStatus.Complete,
|
||||
"cancelled" or "canceled" => LifecycleStatus.Canceled,
|
||||
"onhold" or "on hold" => LifecycleStatus.OnHold,
|
||||
"open" or "incomplete" => LifecycleStatus.Incomplete,
|
||||
"pending" or "onhold" or "on hold" => LifecycleStatus.Pending,
|
||||
"scheduled" => LifecycleStatus.Scheduled,
|
||||
"pendingvendor" or "pending vendor" => LifecycleStatus.PendingVendor,
|
||||
"pendingapproval" or "pending approval" => LifecycleStatus.PendingApproval,
|
||||
"needsreview" or "needs review" => LifecycleStatus.NeedsReview,
|
||||
"closed" => LifecycleStatus.Closed,
|
||||
_ => LifecycleStatus.NeedsReview
|
||||
"en route" or "enroute" => LifecycleStatus.EnRoute,
|
||||
"on site" or "onsite" => LifecycleStatus.OnSite,
|
||||
"inprogress" or "in progress" => LifecycleStatus.InProgress,
|
||||
"completed" or "complete" or "done" or "closed" => LifecycleStatus.Completed,
|
||||
"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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ namespace SeaHaven.Services.Helpers
|
|||
private static readonly HashSet<LifecycleStatus?> ReadOnlyStatuses = new()
|
||||
{
|
||||
LifecycleStatus.Canceled,
|
||||
LifecycleStatus.Closed,
|
||||
LifecycleStatus.Complete
|
||||
LifecycleStatus.Completed
|
||||
};
|
||||
|
||||
public static bool IsReadOnly(LifecycleStatus? status) => ReadOnlyStatuses.Contains(status);
|
||||
|
|
|
|||
|
|
@ -4,13 +4,6 @@ namespace SeaHaven.Services.Helpers
|
|||
{
|
||||
public static class WorkOrderDerivedFields
|
||||
{
|
||||
private static readonly HashSet<LifecycleStatus?> TerminalStatuses = new()
|
||||
{
|
||||
LifecycleStatus.Complete,
|
||||
LifecycleStatus.Canceled,
|
||||
LifecycleStatus.Closed
|
||||
};
|
||||
|
||||
public static bool IsPastDue(
|
||||
DateTime? scheduledDate,
|
||||
LifecycleStatus? lifecycleStatus,
|
||||
|
|
@ -19,7 +12,7 @@ namespace SeaHaven.Services.Helpers
|
|||
if (!scheduledDate.HasValue)
|
||||
return false;
|
||||
|
||||
if (TerminalStatuses.Contains(lifecycleStatus))
|
||||
if (LifecycleStatusMapper.IsTerminal(lifecycleStatus))
|
||||
return false;
|
||||
|
||||
var today = (utcNow ?? DateTime.UtcNow).Date;
|
||||
|
|
@ -27,7 +20,7 @@ namespace SeaHaven.Services.Helpers
|
|||
}
|
||||
|
||||
public static bool IsTerminal(LifecycleStatus? lifecycleStatus)
|
||||
=> TerminalStatuses.Contains(lifecycleStatus);
|
||||
=> LifecycleStatusMapper.IsTerminal(lifecycleStatus);
|
||||
|
||||
public static string? GetDayGroup(DateTime? scheduledDate)
|
||||
{
|
||||
|
|
@ -57,24 +50,6 @@ namespace SeaHaven.Services.Helpers
|
|||
}
|
||||
|
||||
public static string GetLifecycleStatusLabel(LifecycleStatus? status)
|
||||
{
|
||||
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()
|
||||
};
|
||||
}
|
||||
=> status.HasValue ? LifecycleStatusMapper.ToFeLabel(status.Value) : "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
47
SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs
Normal file
47
SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -222,7 +222,7 @@ namespace SeaHaven.Services.Implementation
|
|||
if (string.IsNullOrWhiteSpace(value))
|
||||
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}");
|
||||
|
||||
var old = workOrder.WorkOrderType?.ToString();
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ public class LifecycleStatusMapperTests
|
|||
[InlineData("Open", LifecycleStatus.Incomplete)]
|
||||
[InlineData("InProgress", LifecycleStatus.InProgress)]
|
||||
[InlineData("In Progress", LifecycleStatus.InProgress)]
|
||||
[InlineData("Completed", LifecycleStatus.Complete)]
|
||||
[InlineData("Completed", LifecycleStatus.Completed)]
|
||||
[InlineData("Cancelled", LifecycleStatus.Canceled)]
|
||||
[InlineData("On Hold", LifecycleStatus.OnHold)]
|
||||
[InlineData("On Hold", LifecycleStatus.Pending)]
|
||||
[InlineData("Scheduled", LifecycleStatus.Scheduled)]
|
||||
public void FromLegacyStatus_MapsKnownValues(string input, LifecycleStatus expected)
|
||||
{
|
||||
|
|
@ -19,10 +19,31 @@ public class LifecycleStatusMapperTests
|
|||
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]
|
||||
public void FromLegacyStatus_Unknown_ReturnsNeedsReview()
|
||||
public void FromLegacyStatus_Unknown_ReturnsNull()
|
||||
{
|
||||
var result = LifecycleStatusMapper.FromLegacyStatus("MysteryStatus");
|
||||
Assert.Equal(LifecycleStatus.NeedsReview, result);
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ public class WorkOrderBoardMutationRulesTests
|
|||
{
|
||||
[Theory]
|
||||
[InlineData(LifecycleStatus.Canceled, true)]
|
||||
[InlineData(LifecycleStatus.Closed, true)]
|
||||
[InlineData(LifecycleStatus.Complete, true)]
|
||||
[InlineData(LifecycleStatus.Completed, true)]
|
||||
[InlineData(LifecycleStatus.Scheduled, false)]
|
||||
[InlineData(LifecycleStatus.Incomplete, false)]
|
||||
public void IsReadOnly_ReflectsTerminalStatuses(LifecycleStatus status, bool expected)
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ public class WorkOrderBoardServiceTests
|
|||
Assert.Equal(1, response.Counts.Total);
|
||||
Assert.Single(response.Scheduled);
|
||||
Assert.True(response.Scheduled[0].IsPastDue);
|
||||
Assert.Equal("wednesday", response.Scheduled[0].DayGroup);
|
||||
Assert.Equal("monday", response.Scheduled[0].DayGroup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class WorkOrderDerivedFieldsTests
|
|||
{
|
||||
var result = WorkOrderDerivedFields.IsPastDue(
|
||||
Today.AddDays(-3),
|
||||
LifecycleStatus.Complete,
|
||||
LifecycleStatus.Completed,
|
||||
Today);
|
||||
|
||||
Assert.False(result);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue