diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs
index c3aca36..44f965b 100644
--- a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs
+++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs
@@ -73,8 +73,8 @@ namespace SeaHaven.DataServices.Helpers
if (overdue && !hasTypes)
{
return query.Where(w =>
- w.DueDate != null
- && w.DueDate.Value.Date < today
+ w.ScheduledDate != null
+ && w.ScheduledDate.Value.Date < today
&& w.LifecycleStatus != LifecycleStatus.Completed
&& w.LifecycleStatus != LifecycleStatus.Canceled);
}
@@ -82,8 +82,8 @@ namespace SeaHaven.DataServices.Helpers
if (overdue)
{
return query.Where(w =>
- (w.DueDate != null
- && w.DueDate.Value.Date < today
+ (w.ScheduledDate != null
+ && w.ScheduledDate.Value.Date < today
&& w.LifecycleStatus != LifecycleStatus.Completed
&& w.LifecycleStatus != LifecycleStatus.Canceled)
|| (w.WorkOrderType != null && typeFilters.Contains(w.WorkOrderType.Value))
diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs
index be14d8c..441802b 100644
--- a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs
+++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs
@@ -61,8 +61,8 @@ namespace SeaHaven.DataServices.Helpers
|| (w.IsAddOn && ("add-on".Contains(s) || "addon".Contains(s)))
|| (w.WorkOrderType == WorkOrderType.Other && "other".Contains(s))
|| ("overdue".Contains(s)
- && w.DueDate != null
- && w.DueDate.Value.Date < today
+ && w.ScheduledDate != null
+ && w.ScheduledDate.Value.Date < today
&& w.LifecycleStatus != LifecycleStatus.Completed
&& w.LifecycleStatus != LifecycleStatus.Canceled)
|| (w.LifecycleStatus == LifecycleStatus.Incomplete && "incomplete".Contains(s))
diff --git a/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs b/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
index 9cc77b9..c7ac473 100644
--- a/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
+++ b/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
@@ -1,4 +1,4 @@
-using Data.SeaHavenIndustries;
+using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
namespace SeaHaven.Services.Helpers
@@ -6,38 +6,38 @@ namespace SeaHaven.Services.Helpers
public static class WorkOrderDerivedFields
{
///
- /// Past due overlay from DueDate (deadline) — never a WorkOrderType.
- /// Independent of ScheduledDate (planned work / Schedule On).
+ /// Past due overlay from Schedule On — never a WorkOrderType. Matches SHOC board design (SH-185).
+ /// Changing Due Date alone must never trigger or clear it.
///
public static bool IsPastDue(WorkOrder workOrder, DateOnly? asOf = null)
{
var today = asOf ?? DateOnly.FromDateTime(DateTime.UtcNow);
- if (workOrder.DueDate == null)
+ if (workOrder.ScheduledDate == null)
return false;
if (LifecycleStatusMapper.IsTerminal(workOrder.LifecycleStatus))
return false;
- return DateOnly.FromDateTime(workOrder.DueDate.Value.Date) < today;
+ return DateOnly.FromDateTime(workOrder.ScheduledDate.Value.Date) < today;
}
///
- /// Board read-model past-due check against DueDate. Compares UTC calendar dates;
+ /// Board read-model past-due check against ScheduledDate. Compares UTC calendar dates;
/// business/location timezone support is planned for a later phase.
///
public static bool IsPastDue(
- DateTime? dueDate,
+ DateTime? scheduledDate,
LifecycleStatus? lifecycleStatus,
DateTime? utcNow = null)
{
- if (!dueDate.HasValue)
+ if (!scheduledDate.HasValue)
return false;
if (LifecycleStatusMapper.IsTerminal(lifecycleStatus))
return false;
var today = (utcNow ?? DateTime.UtcNow).Date;
- return dueDate.Value.Date < today;
+ return scheduledDate.Value.Date < today;
}
///
diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs
index 0389f4c..a2bda0c 100644
--- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs
+++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs
@@ -95,7 +95,7 @@ namespace SeaHaven.Services.Implementation
CarriedOver = row.CarriedOver,
IsAddOn = row.IsAddOn,
WorkOrderType = row.WorkOrderType,
- IsPastDue = WorkOrderDerivedFields.IsPastDue(row.DueDate, row.LifecycleStatus, utcNow),
+ IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow),
SiteCode = row.SiteCode,
LocationName = row.LocationName,
PocName = row.PocName,
diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs
index 1306245..fc81b2c 100644
--- a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs
+++ b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs
@@ -61,7 +61,7 @@ namespace SeaHaven.Services.Implementation
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
throw new WorkOrderBoardValidationException("CanceledReadOnly", "Work order is read-only in its current status.");
- var isPastDue = WorkOrderDerivedFields.IsPastDue(workOrder.DueDate, workOrder.LifecycleStatus);
+ var isPastDue = WorkOrderDerivedFields.IsPastDue(workOrder.ScheduledDate, workOrder.LifecycleStatus);
if (WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(canonicalField, isPastDue))
throw new WorkOrderBoardValidationException("PastDueStatusBlocked", "Status cannot be changed while work order is past due. Update due date first.");
diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardConcurrencyTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardConcurrencyTests.cs
index 16f8e9e..d1014b6 100644
--- a/SeaHavenIndustries.Tests/WorkOrderBoardConcurrencyTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderBoardConcurrencyTests.cs
@@ -141,8 +141,11 @@ public class WorkOrderBoardConcurrencyTests
WorkOrderVersion = Convert.ToBase64String(wo!.RowVersion!)
}, "actor-1");
- var auditCount = await context.WorkOrderAuditLogs.CountAsync();
- Assert.Equal(2, auditCount);
+ var audits = await context.WorkOrderAuditLogs.ToListAsync();
+ Assert.Equal(3, audits.Count);
+ Assert.Contains(audits, a => a.FieldName == "IsAddOn");
+ Assert.Contains(audits, a => a.FieldName == "ScheduledDate");
+ Assert.Contains(audits, a => a.FieldName == "RescheduleCount");
}
[Fact]
diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs
index 04aa6c0..9da4328 100644
--- a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs
@@ -379,7 +379,7 @@ public class WorkOrderAdvancedSearchServiceTests
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
Assert.Equal(1, result.TotalCount);
- Assert.Equal(1, result.Items.First().Id);
+ Assert.Equal(3, result.Items.First().Id);
Assert.True(result.Items.First().IsPastDue);
}
@@ -394,8 +394,8 @@ public class WorkOrderAdvancedSearchServiceTests
{
Id = 1,
WorkOrderType = WorkOrderType.PM,
- DueDate = today.AddDays(-3),
- ScheduledDate = today.AddDays(5),
+ DueDate = today.AddDays(5),
+ ScheduledDate = today.AddDays(-3),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000001"
},
@@ -498,9 +498,9 @@ public class WorkOrderAdvancedSearchServiceTests
var overdue = Assert.Single(result.Items);
Assert.Equal(0, result.Page);
- Assert.Equal(2, overdue.Id);
+ Assert.Equal(1, overdue.Id);
Assert.True(overdue.IsPastDue);
- Assert.DoesNotContain(result.Items, i => i.Id == 1);
+ Assert.DoesNotContain(result.Items, i => i.Id == 2);
Assert.DoesNotContain(result.Items, i => i.Id == 3);
Assert.DoesNotContain(result.Items, i => i.Id == 4);
}
diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs
index ddab1d5..69847c5 100644
--- a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs
@@ -55,7 +55,11 @@ public class WorkOrderBoardUpdateServiceTests
}, "actor-1");
Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus);
- Assert.Equal(2, await context.WorkOrderAuditLogs.CountAsync());
+ var audits = await context.WorkOrderAuditLogs.ToListAsync();
+ Assert.Equal(3, audits.Count);
+ Assert.Contains(audits, a => a.FieldName == "IsAddOn");
+ Assert.Contains(audits, a => a.FieldName == "ScheduledDate");
+ Assert.Contains(audits, a => a.Action == AuditActionType.StatusChanged.ToString());
}
[Fact]
@@ -91,8 +95,8 @@ public class WorkOrderBoardUpdateServiceTests
{
Id = 1,
LifecycleStatus = LifecycleStatus.Scheduled,
- DueDate = DateTime.UtcNow.Date.AddDays(-3),
- ScheduledDate = DateTime.UtcNow.Date.AddDays(5),
+ DueDate = DateTime.UtcNow.Date.AddDays(5),
+ ScheduledDate = DateTime.UtcNow.Date.AddDays(-3),
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
};
context.workOrders.Add(wo);
@@ -110,7 +114,7 @@ public class WorkOrderBoardUpdateServiceTests
}
[Fact]
- public async Task PatchField_RescheduleAlone_DoesNotClearPastDueBlock()
+ public async Task PatchField_FutureSchedule_ClearsPastDueAndAllowsStatusChange()
{
var (context, service) = CreateSut();
var wo = new WorkOrder
@@ -132,23 +136,23 @@ public class WorkOrderBoardUpdateServiceTests
WorkOrderVersion = ToVersion(wo)
}, "actor-1");
- Assert.True(row.IsPastDue);
+ Assert.False(row.IsPastDue);
Assert.Equal(DateTime.UtcNow.Date.AddDays(7), row.ScheduledDate!.Value.Date);
wo = await context.workOrders.SingleAsync(w => w.Id == 1);
- var ex = await Assert.ThrowsAsync(() =>
- service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
- {
- Field = WorkOrderBoardFieldNames.LifecycleStatus,
- Value = "Complete",
- WorkOrderVersion = ToVersion(wo)
- }, "actor-1"));
+ var afterStatus = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
+ {
+ Field = WorkOrderBoardFieldNames.LifecycleStatus,
+ Value = "Complete",
+ WorkOrderVersion = ToVersion(wo)
+ }, "actor-1");
- Assert.Equal("PastDueStatusBlocked", ex.Code);
+ Assert.Equal(LifecycleStatus.Completed, afterStatus.LifecycleStatus);
+ Assert.False(afterStatus.IsPastDue);
}
[Fact]
- public async Task PatchField_FutureDueDate_ClearsPastDueAndAllowsStatusChange()
+ public async Task PatchField_DueDateAlone_DoesNotClearPastDue()
{
var (context, service) = CreateSut();
var wo = new WorkOrder
@@ -170,19 +174,19 @@ public class WorkOrderBoardUpdateServiceTests
WorkOrderVersion = ToVersion(wo)
}, "actor-1");
- Assert.False(afterDue.IsPastDue);
+ Assert.True(afterDue.IsPastDue);
Assert.Equal(DateTime.UtcNow.Date.AddDays(-1), afterDue.ScheduledDate!.Value.Date);
wo = await context.workOrders.SingleAsync(w => w.Id == 1);
- var afterStatus = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
- {
- Field = WorkOrderBoardFieldNames.LifecycleStatus,
- Value = "Complete",
- WorkOrderVersion = ToVersion(wo)
- }, "actor-1");
+ var ex = await Assert.ThrowsAsync(() =>
+ service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
+ {
+ Field = WorkOrderBoardFieldNames.LifecycleStatus,
+ Value = "Complete",
+ WorkOrderVersion = ToVersion(wo)
+ }, "actor-1"));
- Assert.Equal(LifecycleStatus.Completed, afterStatus.LifecycleStatus);
- Assert.False(afterStatus.IsPastDue);
+ Assert.Equal("PastDueStatusBlocked", ex.Code);
}
[Fact]
diff --git a/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs b/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs
index 49b5a61..2dd8c6b 100644
--- a/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs
@@ -12,21 +12,7 @@ public class WorkOrderDerivedFieldsTests
private static readonly DateTime TodayDate = DateTime.UtcNow.Date;
[Fact]
- public void IsPastDue_WhenDuePastAndScheduleFuture_ReturnsTrue()
- {
- var wo = new WorkOrder
- {
- DueDate = Past,
- ScheduledDate = Future,
- LifecycleStatus = LifecycleStatus.Scheduled
- };
-
- Assert.True(WorkOrderDerivedFields.IsPastDue(wo, Today));
- Assert.True(WorkOrderDerivedFields.IsPastDue(wo.DueDate, wo.LifecycleStatus));
- }
-
- [Fact]
- public void IsPastDue_WhenDueFutureAndSchedulePast_ReturnsFalse()
+ public void IsPastDue_WhenSchedulePastAndDueFuture_ReturnsTrue()
{
var wo = new WorkOrder
{
@@ -35,16 +21,31 @@ public class WorkOrderDerivedFieldsTests
LifecycleStatus = LifecycleStatus.Scheduled
};
- Assert.False(WorkOrderDerivedFields.IsPastDue(wo, Today));
+ Assert.True(WorkOrderDerivedFields.IsPastDue(wo, Today));
+ Assert.True(WorkOrderDerivedFields.IsPastDue(wo.ScheduledDate, wo.LifecycleStatus));
}
[Fact]
- public void IsPastDue_WhenDueDateNull_ReturnsFalse()
+ public void IsPastDue_WhenScheduleFutureAndDuePast_ReturnsFalse()
{
var wo = new WorkOrder
{
- DueDate = null,
- ScheduledDate = Past,
+ DueDate = Past,
+ ScheduledDate = Future,
+ LifecycleStatus = LifecycleStatus.Scheduled
+ };
+
+ Assert.False(WorkOrderDerivedFields.IsPastDue(wo, Today));
+ Assert.False(WorkOrderDerivedFields.IsPastDue(wo.ScheduledDate, wo.LifecycleStatus));
+ }
+
+ [Fact]
+ public void IsPastDue_WhenScheduledDateNull_ReturnsFalse()
+ {
+ var wo = new WorkOrder
+ {
+ DueDate = Past,
+ ScheduledDate = null,
LifecycleStatus = LifecycleStatus.Scheduled
};
@@ -53,12 +54,12 @@ public class WorkOrderDerivedFieldsTests
}
[Fact]
- public void IsPastDue_WhenDueEqualsToday_ReturnsFalse()
+ public void IsPastDue_WhenScheduleEqualsToday_ReturnsFalse()
{
var wo = new WorkOrder
{
- DueDate = TodayDate,
- ScheduledDate = Past,
+ DueDate = Past,
+ ScheduledDate = TodayDate,
LifecycleStatus = LifecycleStatus.Scheduled
};
diff --git a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs
index 044567b..1d7440e 100644
--- a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs
@@ -111,8 +111,8 @@ public class WorkOrderDetailServiceTests
Id = 1,
InternalWONumber = "00000000001",
LifecycleStatus = LifecycleStatus.Scheduled,
- DueDate = DateTime.UtcNow.AddDays(-2),
- ScheduledDate = DateTime.UtcNow.AddDays(3),
+ DueDate = DateTime.UtcNow.AddDays(3),
+ ScheduledDate = DateTime.UtcNow.AddDays(-2),
Trade = "HVAC PM",
WorkOrderType = WorkOrderType.PM,
DocStatus = DocStatus.No,
diff --git a/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs b/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs
index 539d4ab..c299e21 100644
--- a/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs
+++ b/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs
@@ -310,8 +310,8 @@ public class WorkOrderWeekRolledTests
context.workOrders.Add(new WorkOrder
{
Id = 1,
- DueDate = pastDate,
- ScheduledDate = DateTime.UtcNow.Date.AddDays(5),
+ DueDate = DateTime.UtcNow.Date.AddDays(5),
+ ScheduledDate = pastDate,
LifecycleStatus = LifecycleStatus.Scheduled,
OperationalFlags = OperationalFlags.None
});
@@ -335,8 +335,8 @@ public class WorkOrderWeekRolledTests
context.workOrders.Add(new WorkOrder
{
Id = 1,
- DueDate = futureDate,
- ScheduledDate = DateTime.UtcNow.Date.AddDays(-3),
+ DueDate = DateTime.UtcNow.Date.AddDays(-3),
+ ScheduledDate = futureDate,
LifecycleStatus = LifecycleStatus.Scheduled,
OperationalFlags = OperationalFlags.PastDue
});