mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
Reactive/Emergency work orders with a SEV 1-5 level are timed from their
creation against the SEV Respond deadline (2/4/8/24/72 hours, one backend
table). From 50% they are at risk: a dismissable High row in the "SLA at
Risk" section and an entry in the feed's slaAtRisk set with the server
clock (start, deadline, percent) for the banner and toast. From 100% they
are a Critical acknowledge row that only acknowledging removes.
POST /api/notifications/sla/{id}/acknowledge records who and when as a
work-order audit entry ("SLA breach acknowledged by <name>"), scoped to the
caller's feed audience: 404 outside it, 409 before the deadline, 204 when
recorded or already recorded. A later severity change is a new breach.
55 lines
2.6 KiB
C#
55 lines
2.6 KiB
C#
namespace SeaHaven.Services.Helpers
|
||
{
|
||
public enum SlaState
|
||
{
|
||
OnTrack,
|
||
/// <summary>At least half of the response window has elapsed.</summary>
|
||
AtRisk,
|
||
/// <summary>The whole response window has elapsed: the response deadline was missed.</summary>
|
||
Breached
|
||
}
|
||
|
||
/// <summary>Where one work order stands against its SEV response window.</summary>
|
||
public sealed record SlaClock(int Severity, DateTime StartedAt, DateTime DeadlineAt, int PercentElapsed, SlaState State);
|
||
|
||
/// <summary>
|
||
/// Respond deadlines from the client's SEV table, the same values the severity badge tooltip shows.
|
||
/// They apply to Reactive and Emergency work orders with a severity, and the clock starts when the
|
||
/// work order is created, never at its scheduled date. The table's "next day if after hours" note
|
||
/// is not modelled: the clock always counts the plain hours.
|
||
/// </summary>
|
||
public static class SlaResponseWindows
|
||
{
|
||
public static readonly IReadOnlyDictionary<int, TimeSpan> Respond = new Dictionary<int, TimeSpan>
|
||
{
|
||
[1] = TimeSpan.FromHours(2),
|
||
[2] = TimeSpan.FromHours(4),
|
||
[3] = TimeSpan.FromHours(8),
|
||
[4] = TimeSpan.FromHours(24),
|
||
[5] = TimeSpan.FromHours(72)
|
||
};
|
||
|
||
/// <summary>The latest creation time at which a work order of this level has used half its window.</summary>
|
||
public static DateTime AtRiskCreatedAtOrBefore(int severity, DateTime now) => now - Respond[severity] / 2;
|
||
|
||
/// <summary>The latest creation time at which a work order of this level has missed its deadline.</summary>
|
||
public static DateTime BreachedCreatedAtOrBefore(int severity, DateTime now) => now - Respond[severity];
|
||
|
||
/// <summary>Null when the severity is not a SEV 1–5 level.</summary>
|
||
public static SlaClock? Evaluate(string? severity, DateTime createdAt, DateTime now)
|
||
{
|
||
var level = WorkOrderSeverityRules.ParseLevel(severity);
|
||
if (level is not int sev)
|
||
return null;
|
||
|
||
var window = Respond[sev];
|
||
var startedAt = DateTime.SpecifyKind(createdAt, DateTimeKind.Utc);
|
||
var elapsed = now - startedAt;
|
||
var state = elapsed >= window
|
||
? SlaState.Breached
|
||
: elapsed >= window / 2 ? SlaState.AtRisk : SlaState.OnTrack;
|
||
var percent = (int)Math.Floor(Math.Max(elapsed.Ticks, 0) * 100d / window.Ticks);
|
||
return new SlaClock(sev, startedAt, startedAt + window, percent, state);
|
||
}
|
||
}
|
||
}
|