diff --git a/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs b/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs
index dde4bfb..e1cec9d 100644
--- a/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs
+++ b/SeaHaven.Services/Helpers/WorkOrderBoardMutationRules.cs
@@ -46,7 +46,13 @@ namespace SeaHaven.Services.Helpers
=> field.Equals(WorkOrderBoardFieldNames.LifecycleStatus, StringComparison.OrdinalIgnoreCase) && isPastDue;
///
- /// SHOC rule: Incomplete + specific scheduled date + assignee → Scheduled.
+ /// Concrete Schedule On: a calendar day is set and the row is not week-only.
+ ///
+ public static bool HasConcreteSchedule(DateTime? scheduledDate, bool? scheduleWeekOnly)
+ => scheduledDate.HasValue && scheduleWeekOnly != true;
+
+ ///
+ /// Incomplete or Pending + concrete scheduled date → Scheduled. Assignee is not required.
/// Week-only targets do not auto-schedule (same as ).
///
public static bool ShouldAutoSchedule(
@@ -54,10 +60,27 @@ namespace SeaHaven.Services.Helpers
DateTime? scheduledDate,
string? assignTo,
bool? scheduleWeekOnly = null)
- => status == LifecycleStatus.Incomplete
- && scheduleWeekOnly != true
- && scheduledDate.HasValue
- && !string.IsNullOrWhiteSpace(assignTo);
+ {
+ _ = assignTo;
+ return ShouldPromoteToScheduled(status, scheduledDate, scheduleWeekOnly);
+ }
+
+ public static bool ShouldPromoteToScheduled(
+ LifecycleStatus? status,
+ DateTime? scheduledDate,
+ bool? scheduleWeekOnly = null)
+ => (status == LifecycleStatus.Incomplete || status == LifecycleStatus.Pending)
+ && HasConcreteSchedule(scheduledDate, scheduleWeekOnly);
+
+ ///
+ /// Scheduled without a concrete date (cleared or week-only) → Pending. Later statuses are ignored.
+ ///
+ public static bool ShouldDemoteFromScheduled(
+ LifecycleStatus? status,
+ DateTime? scheduledDate,
+ bool? scheduleWeekOnly = null)
+ => status == LifecycleStatus.Scheduled
+ && !HasConcreteSchedule(scheduledDate, scheduleWeekOnly);
public static bool IsReschedule(DateTime? previousDate, DateTime? newDate)
=> previousDate.HasValue
diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs
index 6494004..548a50f 100644
--- a/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderBoardMutationRulesTests.cs
@@ -27,12 +27,21 @@ public class WorkOrderBoardMutationRulesTests
}
[Fact]
- public void ShouldAutoSchedule_WhenIncompleteWithDateAndAssignee()
+ public void ShouldAutoSchedule_WhenIncompleteWithDate_DoesNotRequireAssignee()
{
Assert.True(WorkOrderBoardMutationRules.ShouldAutoSchedule(
LifecycleStatus.Incomplete,
new DateTime(2026, 6, 25),
- "dispatcher-1"));
+ assignTo: null));
+ }
+
+ [Fact]
+ public void ShouldAutoSchedule_WhenPendingWithConcreteDate()
+ {
+ Assert.True(WorkOrderBoardMutationRules.ShouldAutoSchedule(
+ LifecycleStatus.Pending,
+ new DateTime(2026, 6, 25),
+ assignTo: null));
}
[Fact]
@@ -41,7 +50,7 @@ public class WorkOrderBoardMutationRulesTests
Assert.False(WorkOrderBoardMutationRules.ShouldAutoSchedule(
LifecycleStatus.Scheduled,
new DateTime(2026, 6, 25),
- "dispatcher-1"));
+ assignTo: null));
}
[Fact]
@@ -50,10 +59,52 @@ public class WorkOrderBoardMutationRulesTests
Assert.False(WorkOrderBoardMutationRules.ShouldAutoSchedule(
LifecycleStatus.Incomplete,
new DateTime(2026, 6, 25),
- "dispatcher-1",
+ assignTo: null,
scheduleWeekOnly: true));
}
+ [Fact]
+ public void ShouldAutoSchedule_FalseWhenInProgress()
+ {
+ Assert.False(WorkOrderBoardMutationRules.ShouldAutoSchedule(
+ LifecycleStatus.InProgress,
+ new DateTime(2026, 6, 25),
+ assignTo: null));
+ }
+
+ [Fact]
+ public void ShouldDemoteFromScheduled_WhenDateCleared()
+ {
+ Assert.True(WorkOrderBoardMutationRules.ShouldDemoteFromScheduled(
+ LifecycleStatus.Scheduled,
+ scheduledDate: null));
+ }
+
+ [Fact]
+ public void ShouldDemoteFromScheduled_WhenWeekOnly()
+ {
+ Assert.True(WorkOrderBoardMutationRules.ShouldDemoteFromScheduled(
+ LifecycleStatus.Scheduled,
+ new DateTime(2026, 6, 25),
+ scheduleWeekOnly: true));
+ }
+
+ [Fact]
+ public void ShouldDemoteFromScheduled_FalseWhenConcreteDateRemains()
+ {
+ Assert.False(WorkOrderBoardMutationRules.ShouldDemoteFromScheduled(
+ LifecycleStatus.Scheduled,
+ new DateTime(2026, 6, 25)));
+ }
+
+ [Fact]
+ public void ShouldDemoteFromScheduled_FalseWhenInProgress()
+ {
+ Assert.False(WorkOrderBoardMutationRules.ShouldDemoteFromScheduled(
+ LifecycleStatus.InProgress,
+ scheduledDate: null));
+ }
+
[Fact]
public void CanPatchBoardField_AllowsOnlyFlagColorWhenCompleted()
{