shoc-backend/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs
Arthur Bassi c9a80f5c79 fix(work-orders): restore SH-185 Schedule On Past Due and assert IsAddOn audit
Past Due follows ScheduledDate so Due Date alone cannot set or clear it. Auto-schedule and reschedule tests now expect the third IsAddOn audit field.
2026-08-17 10:36:03 -03:00

108 lines
3.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
namespace SeaHaven.Services.Helpers
{
public static class WorkOrderDerivedFields
{
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Board read-model past-due check against ScheduledDate. Compares UTC calendar dates;
/// business/location timezone support is planned for a later phase.
/// </summary>
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;
}
/// <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>
/// SHOC rule: specific scheduled date + assignee + Incomplete → Scheduled.
/// Week-only targets do not auto-schedule.
/// </summary>
public static bool ApplyAutoScheduleIfEligible(WorkOrder workOrder)
{
if (workOrder.LifecycleStatus != LifecycleStatus.Incomplete)
return false;
if (workOrder.ScheduleWeekOnly == true)
return false;
if (workOrder.ScheduledDate == null)
return false;
if (string.IsNullOrWhiteSpace(workOrder.AssignTo))
return false;
workOrder.LifecycleStatus = LifecycleStatus.Scheduled;
return true;
}
/// <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>
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) : "";
}
}