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);