diff --git a/SeaHaven.Services/Helpers/WorkOrderBoardApptTimeParser.cs b/SeaHaven.Services/Helpers/WorkOrderBoardApptTimeParser.cs index f50949a..a3a2694 100644 --- a/SeaHaven.Services/Helpers/WorkOrderBoardApptTimeParser.cs +++ b/SeaHaven.Services/Helpers/WorkOrderBoardApptTimeParser.cs @@ -53,5 +53,8 @@ namespace SeaHaven.Services.Helpers return date.Value.Date.Add(time.Value); } + + public static DateTime? ToScheduledInstant(DateTime? date, TimeSpan? time) + => time.HasValue ? CombineDateAndTime(date, time) : null; } } diff --git a/SeaHaven.Services/Helpers/WorkOrderBoardFieldMutations.cs b/SeaHaven.Services/Helpers/WorkOrderBoardFieldMutations.cs index 2477cb3..c3a59a0 100644 --- a/SeaHaven.Services/Helpers/WorkOrderBoardFieldMutations.cs +++ b/SeaHaven.Services/Helpers/WorkOrderBoardFieldMutations.cs @@ -87,8 +87,8 @@ namespace SeaHaven.Services.Helpers var oldStart = FormatDateTime(workOrder.ScheduledStart); var oldEnd = FormatDateTime(workOrder.ScheduledEnd); - workOrder.ScheduledStart = WorkOrderBoardApptTimeParser.CombineDateAndTime(apptDate, start); - workOrder.ScheduledEnd = WorkOrderBoardApptTimeParser.CombineDateAndTime(apptDate, end); + workOrder.ScheduledStart = WorkOrderBoardApptTimeParser.ToScheduledInstant(apptDate, start); + workOrder.ScheduledEnd = WorkOrderBoardApptTimeParser.ToScheduledInstant(apptDate, end); var newStart = FormatDateTime(workOrder.ScheduledStart); var newEnd = FormatDateTime(workOrder.ScheduledEnd); diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs index 972e2f1..8e8aa7d 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -85,8 +85,10 @@ namespace SeaHaven.Services.Implementation public static WorkOrderBoardRowDto MapRawRow(WorkOrderBoardRawRow row, DateTime utcNow) { // Appt column: date prefers dispatch appointment (vendor slot), then WO ScheduledDate. - // Time prefers WO ScheduledStart/End (dispatcher window), then dispatch datetime. - var apptStart = row.ScheduledStart ?? row.DispatchApptDate; + // Time uses WO ScheduledStart/End only. Do not fall back to a date-only dispatch + // instant (midnight), or clearing apptTime still renders 00:00. + var apptStart = row.ScheduledStart + ?? (HasClockTime(row.DispatchApptDate) ? row.DispatchApptDate : null); var apptEnd = row.ScheduledEnd; var (vendorId, vendorName) = WorkOrderBoardDispatchAssignment.LiveVendor( row.VendorId, @@ -145,6 +147,9 @@ namespace SeaHaven.Services.Implementation }; } + private static bool HasClockTime(DateTime? value) + => value.HasValue && value.Value.TimeOfDay != TimeSpan.Zero; + private static string FormatName(string? firstName, string? lastName) => $"{firstName ?? ""} {lastName ?? ""}".Trim(); diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs index b8c94e8..12e76ec 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs @@ -651,8 +651,8 @@ namespace SeaHaven.Services.Implementation var oldEnd = FormatDateTime(workOrder.ScheduledEnd); var dispatchId = ResolveDispatchIdForAudit(dispatch); - workOrder.ScheduledStart = WorkOrderBoardApptTimeParser.CombineDateAndTime(apptDate, start); - workOrder.ScheduledEnd = WorkOrderBoardApptTimeParser.CombineDateAndTime(apptDate, end); + workOrder.ScheduledStart = WorkOrderBoardApptTimeParser.ToScheduledInstant(apptDate, start); + workOrder.ScheduledEnd = WorkOrderBoardApptTimeParser.ToScheduledInstant(apptDate, end); var newStart = FormatDateTime(workOrder.ScheduledStart); var newEnd = FormatDateTime(workOrder.ScheduledEnd); diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardFieldMutationsTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardFieldMutationsTests.cs new file mode 100644 index 0000000..aa83201 --- /dev/null +++ b/SeaHavenIndustries.Tests/WorkOrderBoardFieldMutationsTests.cs @@ -0,0 +1,53 @@ +using Data.SeaHavenIndustries; +using SeaHaven.Services.Helpers; + +namespace SeaHavenIndustries.Tests; + +public class WorkOrderBoardFieldMutationsTests +{ + [Fact] + public void ApplyApptTime_EmptyValue_ClearsStartAndEnd_KeepsDates() + { + var appointmentDate = new DateTime(2026, 6, 25); + var workOrder = new WorkOrder + { + ScheduledDate = appointmentDate, + ScheduledStart = new DateTime(2026, 6, 25, 9, 0, 0), + ScheduledEnd = new DateTime(2026, 6, 25, 11, 0, 0) + }; + var dispatch = new Dispatch { Id = 10, ScheduledDate = appointmentDate }; + + WorkOrderBoardFieldMutations.ApplyApptTime(workOrder, dispatch, ""); + + Assert.Null(workOrder.ScheduledStart); + Assert.Null(workOrder.ScheduledEnd); + Assert.Equal(appointmentDate, workOrder.ScheduledDate); + Assert.Equal(appointmentDate, dispatch.ScheduledDate); + } + + [Fact] + public void ApplyApptTime_StartOnly_LeavesEndNull() + { + var appointmentDate = new DateTime(2026, 6, 25); + var workOrder = new WorkOrder { ScheduledDate = appointmentDate }; + var dispatch = new Dispatch { Id = 10, ScheduledDate = appointmentDate }; + + WorkOrderBoardFieldMutations.ApplyApptTime(workOrder, dispatch, "09:00"); + + Assert.Equal(new DateTime(2026, 6, 25, 9, 0, 0), workOrder.ScheduledStart); + Assert.Null(workOrder.ScheduledEnd); + } + + [Fact] + public void ApplyApptTime_Range_SetsStartAndEnd() + { + var appointmentDate = new DateTime(2026, 6, 25); + var workOrder = new WorkOrder { ScheduledDate = appointmentDate }; + var dispatch = new Dispatch { Id = 10, ScheduledDate = appointmentDate }; + + WorkOrderBoardFieldMutations.ApplyApptTime(workOrder, dispatch, "09:00 – 11:00"); + + Assert.Equal(new DateTime(2026, 6, 25, 9, 0, 0), workOrder.ScheduledStart); + Assert.Equal(new DateTime(2026, 6, 25, 11, 0, 0), workOrder.ScheduledEnd); + } +} diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs index 9bdf934..f1f2732 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs @@ -1070,6 +1070,53 @@ public class WorkOrderBoardUpdateServiceTests Assert.Equal(new DateTime(2026, 6, 25, 10, 0, 0), reloaded.ScheduledEnd); } + [Fact] + public async Task PatchField_ApptTime_EmptyClearsStartAndEnd_KeepsAppointmentDate() + { + var (context, service) = CreateSut(); + var appointmentDate = new DateTime(2026, 6, 25); + var dispatch = new Dispatch + { + Id = 10, + WorkOrderId = 1, + VendorId = 1, + ScheduledDate = appointmentDate, + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } + }; + var wo = new WorkOrder + { + Id = 1, + LifecycleStatus = LifecycleStatus.Scheduled, + PrimaryDispatchId = 10, + ScheduledDate = appointmentDate, + ScheduledStart = new DateTime(2026, 6, 25, 9, 0, 0), + ScheduledEnd = new DateTime(2026, 6, 25, 11, 0, 0), + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } + }; + context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Vendor A" }); + context.Dispatches.Add(dispatch); + context.workOrders.Add(wo); + await context.SaveChangesAsync(); + + var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.ApptTime, + Value = "", + WorkOrderVersion = ToVersion(wo), + DispatchVersion = ToVersion(dispatch), + PrimaryDispatchId = 10 + }, "actor-1"); + + Assert.True(string.IsNullOrEmpty(result.ApptTime)); + Assert.Equal(appointmentDate, result.ScheduledDate); + var reloaded = await context.workOrders.FindAsync(1); + Assert.Null(reloaded!.ScheduledStart); + Assert.Null(reloaded.ScheduledEnd); + Assert.Equal(appointmentDate, reloaded.ScheduledDate); + var reloadedDispatch = await context.Dispatches.FindAsync(10); + Assert.Equal(appointmentDate, reloadedDispatch!.ScheduledDate); + } + [Fact] public async Task PatchField_ApptTime_DashPrefixedToken_TreatedAsSingleStart() {