using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; namespace SeaHaven.Services.Helpers { public static class WorkOrderDerivedFields { /// /// 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.ScheduledDate == null) return false; if (LifecycleStatusMapper.IsTerminal(workOrder.LifecycleStatus)) return false; return DateOnly.FromDateTime(workOrder.ScheduledDate.Value.Date) < today; } /// /// 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? scheduledDate, LifecycleStatus? lifecycleStatus, DateTime? utcNow = null) { if (!scheduledDate.HasValue) return false; if (LifecycleStatusMapper.IsTerminal(lifecycleStatus)) return false; var today = (utcNow ?? DateTime.UtcNow).Date; return scheduledDate.Value.Date < today; } /// /// Sets initial lifecycle on create. New WOs start Incomplete unless auto-scheduled. /// public static void SetInitialLifecycleStatus(WorkOrder workOrder) { workOrder.LifecycleStatus ??= LifecycleStatus.Incomplete; ApplyAutoScheduleIfEligible(workOrder); } /// /// 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) { var before = workOrder.LifecycleStatus; ApplyLifecycleFromSchedule(workOrder); return before != LifecycleStatus.Scheduled && workOrder.LifecycleStatus == LifecycleStatus.Scheduled; } /// /// Maps scheduled date to Mon–Fri board column. Phase 1 uses the stored /// date's day-of-week (UTC); timezone-aware grouping is planned for a later phase. /// public static string? GetDayGroup(DateTime? scheduledDate) { if (!scheduledDate.HasValue) return null; return scheduledDate.Value.DayOfWeek switch { DayOfWeek.Monday => "monday", DayOfWeek.Tuesday => "tuesday", DayOfWeek.Wednesday => "wednesday", DayOfWeek.Thursday => "thursday", DayOfWeek.Friday => "friday", _ => null }; } public static string? FormatApptTime(DateTime? start, DateTime? end) { if (!start.HasValue) return null; if (end.HasValue) return $"{start.Value:HH:mm} – {end.Value:HH:mm}"; return start.Value.ToString("HH:mm"); } public static string GetLifecycleStatusLabel(LifecycleStatus? status) => status.HasValue ? LifecycleStatusMapper.ToFeLabel(status.Value) : ""; } }