mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 23:35:43 +00:00
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
import type { LicenseApplication } from '@ema-platform/api';
|
|
|
|
/** Amber once this much of the window has been consumed. */
|
|
const WARNING_RATIO = 0.7;
|
|
|
|
const HOUR_MS = 60 * 60 * 1000;
|
|
|
|
export interface SlaState {
|
|
state: 'ok' | 'warning' | 'breached' | 'untracked' | 'decided';
|
|
/** Mantine colour. Always paired with `label` — never colour alone. */
|
|
color: string;
|
|
/** Short text for the badge, e.g. "2d left" or "Overdue 6h". */
|
|
label: string;
|
|
/** The full explanation, including the target, for the tooltip. */
|
|
tooltip: string;
|
|
/** Fraction of the window used, clamped to 0..1. */
|
|
ratio: number;
|
|
}
|
|
|
|
function formatDuration(ms: number): string {
|
|
const hours = Math.floor(Math.abs(ms) / HOUR_MS);
|
|
if (hours < 1) return '<1h';
|
|
if (hours < 48) return `${hours}h`;
|
|
return `${Math.floor(hours / 24)}d`;
|
|
}
|
|
|
|
/**
|
|
* How an application is tracking against its licence type's SLA.
|
|
*
|
|
* Types with no `slaHours` are untracked rather than instantly overdue — the
|
|
* authority has not set a target for them, which is not the same as missing
|
|
* one. Decided applications stop the clock: an approval that took three weeks
|
|
* is history, not an outstanding breach.
|
|
*/
|
|
export function computeSla(
|
|
application: LicenseApplication,
|
|
now: number = Date.now(),
|
|
): SlaState {
|
|
const slaHours = application.licenseType?.slaHours;
|
|
const submittedAt = application.submittedAt;
|
|
|
|
if (!slaHours || !submittedAt) {
|
|
return {
|
|
state: 'untracked',
|
|
color: 'gray',
|
|
label: '—',
|
|
tooltip: 'No turnaround target is set for this licence type.',
|
|
ratio: 0,
|
|
};
|
|
}
|
|
|
|
const submitted = new Date(submittedAt).getTime();
|
|
const target = submitted + slaHours * HOUR_MS;
|
|
const elapsed = (application.decidedAt ? new Date(application.decidedAt).getTime() : now) - submitted;
|
|
const window = slaHours * HOUR_MS;
|
|
const ratio = Math.min(Math.max(elapsed / window, 0), 1);
|
|
const targetText = `Target ${slaHours}h from submission (${new Date(target).toLocaleString()})`;
|
|
|
|
if (application.decidedAt) {
|
|
const met = elapsed <= window;
|
|
return {
|
|
state: 'decided',
|
|
color: met ? 'teal' : 'gray',
|
|
label: met ? 'Met' : 'Missed',
|
|
tooltip: `Decided in ${formatDuration(elapsed)}. ${targetText}`,
|
|
ratio,
|
|
};
|
|
}
|
|
|
|
const remaining = target - now;
|
|
if (remaining < 0) {
|
|
return {
|
|
state: 'breached',
|
|
color: 'red',
|
|
label: `Overdue ${formatDuration(remaining)}`,
|
|
tooltip: `Overdue by ${formatDuration(remaining)}. ${targetText}`,
|
|
ratio: 1,
|
|
};
|
|
}
|
|
|
|
const used = elapsed / window;
|
|
return {
|
|
state: used >= WARNING_RATIO ? 'warning' : 'ok',
|
|
color: used >= WARNING_RATIO ? 'yellow' : 'teal',
|
|
label: `${formatDuration(remaining)} left`,
|
|
tooltip: `${formatDuration(remaining)} remaining. ${targetText}`,
|
|
ratio,
|
|
};
|
|
}
|