diff --git a/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs b/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
index c7ac473..9ef534c 100644
--- a/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
+++ b/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
@@ -50,25 +50,50 @@ namespace SeaHaven.Services.Helpers
}
///
- /// SHOC rule: specific scheduled date + assignee + Incomplete → Scheduled.
- /// Week-only targets do not auto-schedule.
+ /// Promote Incomplete/Pending to Scheduled, or demote Scheduled to Pending, from Schedule On.
+ /// Assignee is not required. Week-only is not a concrete date.
+ ///
+ public static List ApplyLifecycleFromSchedule(WorkOrder workOrder)
+ {
+ var changes = new List();
+ LifecycleStatus? next = null;
+ if (WorkOrderBoardMutationRules.ShouldPromoteToScheduled(
+ workOrder.LifecycleStatus,
+ workOrder.ScheduledDate,
+ workOrder.ScheduleWeekOnly))
+ {
+ next = LifecycleStatus.Scheduled;
+ }
+ else if (WorkOrderBoardMutationRules.ShouldDemoteFromScheduled(
+ workOrder.LifecycleStatus,
+ workOrder.ScheduledDate,
+ workOrder.ScheduleWeekOnly))
+ {
+ next = LifecycleStatus.Pending;
+ }
+
+ if (next == null || workOrder.LifecycleStatus == next)
+ return changes;
+
+ var old = workOrder.LifecycleStatus?.ToString();
+ workOrder.LifecycleStatus = next;
+ workOrder.Status = GetLifecycleStatusLabel(next);
+ changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.StatusChanged(
+ "LifecycleStatus",
+ old,
+ next.Value.ToString()));
+ return changes;
+ }
+
+ ///
+ /// Applies schedule-driven lifecycle (promote or demote). True when the row becomes Scheduled.
///
public static bool ApplyAutoScheduleIfEligible(WorkOrder workOrder)
{
- if (workOrder.LifecycleStatus != LifecycleStatus.Incomplete)
- return false;
-
- if (workOrder.ScheduleWeekOnly == true)
- return false;
-
- if (workOrder.ScheduledDate == null)
- return false;
-
- if (string.IsNullOrWhiteSpace(workOrder.AssignTo))
- return false;
-
- workOrder.LifecycleStatus = LifecycleStatus.Scheduled;
- return true;
+ var before = workOrder.LifecycleStatus;
+ ApplyLifecycleFromSchedule(workOrder);
+ return before != LifecycleStatus.Scheduled
+ && workOrder.LifecycleStatus == LifecycleStatus.Scheduled;
}
///
diff --git a/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs b/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs
index 2dd8c6b..eadc960 100644
--- a/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs
@@ -83,19 +83,48 @@ public class WorkOrderDerivedFieldsTests
}
[Fact]
- public void ApplyAutoSchedule_WhenDateAndAssignee_PromotesToScheduled()
+ public void ApplyAutoSchedule_WhenDateWithoutAssignee_PromotesToScheduled()
{
var wo = new WorkOrder
{
LifecycleStatus = LifecycleStatus.Incomplete,
- ScheduledDate = new DateTime(2026, 6, 25),
- AssignTo = "dispatcher-1"
+ ScheduledDate = new DateTime(2026, 6, 25)
};
Assert.True(WorkOrderDerivedFields.ApplyAutoScheduleIfEligible(wo));
Assert.Equal(LifecycleStatus.Scheduled, wo.LifecycleStatus);
}
+ [Fact]
+ public void ApplyLifecycleFromSchedule_PendingWithDate_PromotesToScheduled()
+ {
+ var wo = new WorkOrder
+ {
+ LifecycleStatus = LifecycleStatus.Pending,
+ ScheduledDate = new DateTime(2026, 6, 25)
+ };
+
+ var changes = WorkOrderDerivedFields.ApplyLifecycleFromSchedule(wo);
+ Assert.Equal(LifecycleStatus.Scheduled, wo.LifecycleStatus);
+ Assert.Single(changes);
+ Assert.Equal(AuditActionType.StatusChanged, changes[0].Action);
+ }
+
+ [Fact]
+ public void ApplyLifecycleFromSchedule_ScheduledWithoutDate_DemotesToPending()
+ {
+ var wo = new WorkOrder
+ {
+ LifecycleStatus = LifecycleStatus.Scheduled,
+ ScheduledDate = null
+ };
+
+ var changes = WorkOrderDerivedFields.ApplyLifecycleFromSchedule(wo);
+ Assert.Equal(LifecycleStatus.Pending, wo.LifecycleStatus);
+ Assert.Single(changes);
+ Assert.Equal(LifecycleStatus.Pending.ToString(), changes[0].NewValue);
+ }
+
[Fact]
public void ApplyAutoSchedule_WeekOnly_DoesNotPromote()
{
@@ -103,7 +132,6 @@ public class WorkOrderDerivedFieldsTests
{
LifecycleStatus = LifecycleStatus.Incomplete,
ScheduledDate = new DateTime(2026, 6, 25),
- AssignTo = "dispatcher-1",
ScheduleWeekOnly = true,
TargetWeek = new DateOnly(2026, 6, 22)
};
@@ -112,13 +140,25 @@ public class WorkOrderDerivedFieldsTests
Assert.Equal(LifecycleStatus.Incomplete, wo.LifecycleStatus);
}
+ [Fact]
+ public void ApplyLifecycleFromSchedule_InProgress_UnchangedWhenDateCleared()
+ {
+ var wo = new WorkOrder
+ {
+ LifecycleStatus = LifecycleStatus.InProgress,
+ ScheduledDate = null
+ };
+
+ Assert.Empty(WorkOrderDerivedFields.ApplyLifecycleFromSchedule(wo));
+ Assert.Equal(LifecycleStatus.InProgress, wo.LifecycleStatus);
+ }
+
[Fact]
public void SetInitialLifecycleStatus_NewWo_StartsIncompleteThenAutoSchedules()
{
var wo = new WorkOrder
{
- ScheduledDate = new DateTime(2026, 6, 25),
- AssignTo = "dispatcher-1"
+ ScheduledDate = new DateTime(2026, 6, 25)
};
WorkOrderDerivedFields.SetInitialLifecycleStatus(wo);