2026-08-17 10:36:03 -03:00
|
|
|
|
using Data.SeaHavenIndustries;
|
2026-07-07 10:10:06 -03:00
|
|
|
|
using Data.SeaHavenIndustries.Enums;
|
|
|
|
|
|
|
|
|
|
|
|
namespace SeaHaven.Services.Helpers
|
|
|
|
|
|
{
|
|
|
|
|
|
public static class WorkOrderDerivedFields
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
2026-08-17 10:36:03 -03:00
|
|
|
|
/// 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.
|
2026-07-07 10:10:06 -03:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static bool IsPastDue(WorkOrder workOrder, DateOnly? asOf = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var today = asOf ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
2026-08-17 10:36:03 -03:00
|
|
|
|
if (workOrder.ScheduledDate == null)
|
2026-07-07 10:10:06 -03:00
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
if (LifecycleStatusMapper.IsTerminal(workOrder.LifecycleStatus))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
2026-08-17 10:36:03 -03:00
|
|
|
|
return DateOnly.FromDateTime(workOrder.ScheduledDate.Value.Date) < today;
|
2026-07-07 10:10:06 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 10:06:36 -03:00
|
|
|
|
/// <summary>
|
2026-08-17 10:36:03 -03:00
|
|
|
|
/// Board read-model past-due check against ScheduledDate. Compares UTC calendar dates;
|
2026-07-09 10:06:36 -03:00
|
|
|
|
/// business/location timezone support is planned for a later phase.
|
|
|
|
|
|
/// </summary>
|
2026-07-07 10:10:06 -03:00
|
|
|
|
public static bool IsPastDue(
|
2026-08-17 10:36:03 -03:00
|
|
|
|
DateTime? scheduledDate,
|
2026-07-07 10:10:06 -03:00
|
|
|
|
LifecycleStatus? lifecycleStatus,
|
|
|
|
|
|
DateTime? utcNow = null)
|
|
|
|
|
|
{
|
2026-08-17 10:36:03 -03:00
|
|
|
|
if (!scheduledDate.HasValue)
|
2026-07-07 10:10:06 -03:00
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
if (LifecycleStatusMapper.IsTerminal(lifecycleStatus))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
var today = (utcNow ?? DateTime.UtcNow).Date;
|
2026-08-17 10:36:03 -03:00
|
|
|
|
return scheduledDate.Value.Date < today;
|
2026-07-07 10:10:06 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Sets initial lifecycle on create. New WOs start Incomplete unless auto-scheduled.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static void SetInitialLifecycleStatus(WorkOrder workOrder)
|
|
|
|
|
|
{
|
|
|
|
|
|
workOrder.LifecycleStatus ??= LifecycleStatus.Incomplete;
|
|
|
|
|
|
ApplyAutoScheduleIfEligible(workOrder);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-09-08 16:21:14 -03:00
|
|
|
|
/// Promote Incomplete/Pending to Scheduled, or demote Scheduled to Pending, from Schedule On.
|
|
|
|
|
|
/// Assignee is not required. Week-only is not a concrete date.
|
2026-07-07 10:10:06 -03:00
|
|
|
|
/// </summary>
|
2026-09-08 16:21:14 -03:00
|
|
|
|
public static List<WorkOrderBoardFieldMutations.BoardFieldChange> ApplyLifecycleFromSchedule(WorkOrder workOrder)
|
2026-07-07 10:10:06 -03:00
|
|
|
|
{
|
2026-09-08 16:21:14 -03:00
|
|
|
|
var changes = new List<WorkOrderBoardFieldMutations.BoardFieldChange>();
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-07-07 10:10:06 -03:00
|
|
|
|
|
2026-09-08 16:21:14 -03:00
|
|
|
|
if (next == null || workOrder.LifecycleStatus == next)
|
|
|
|
|
|
return changes;
|
2026-07-07 10:10:06 -03:00
|
|
|
|
|
2026-09-08 16:21:14 -03:00
|
|
|
|
var old = workOrder.LifecycleStatus?.ToString();
|
|
|
|
|
|
workOrder.LifecycleStatus = next;
|
|
|
|
|
|
workOrder.Status = GetLifecycleStatusLabel(next);
|
|
|
|
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.StatusChanged(
|
|
|
|
|
|
"LifecycleStatus",
|
|
|
|
|
|
old,
|
|
|
|
|
|
next.Value.ToString()));
|
|
|
|
|
|
return changes;
|
|
|
|
|
|
}
|
2026-07-07 10:10:06 -03:00
|
|
|
|
|
2026-09-08 16:21:14 -03:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Applies schedule-driven lifecycle (promote or demote). True when the row becomes Scheduled.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static bool ApplyAutoScheduleIfEligible(WorkOrder workOrder)
|
|
|
|
|
|
{
|
|
|
|
|
|
var before = workOrder.LifecycleStatus;
|
|
|
|
|
|
ApplyLifecycleFromSchedule(workOrder);
|
|
|
|
|
|
return before != LifecycleStatus.Scheduled
|
|
|
|
|
|
&& workOrder.LifecycleStatus == LifecycleStatus.Scheduled;
|
2026-07-07 10:10:06 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 10:06:36 -03:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 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.
|
|
|
|
|
|
/// </summary>
|
2026-07-07 10:10:06 -03:00
|
|
|
|
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) : "";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|