shoc-frontend-new/src/domain/work-orders/utils/board-audit-action-rules.ts
2026-09-16 16:56:26 -03:00

200 lines
5.9 KiB
TypeScript

import { SEVERITY_LEVELS } from "@/domain/work-orders/types/work-order-severity";
import type { WorkOrderAuditEntry } from "@/domain/work-orders/types/work-order-detail";
export type AuditActionSource = Pick<
WorkOrderAuditEntry,
"action" | "fieldName" | "oldValue" | "newValue"
>;
export type FormattedAuditValue = {
text: string;
/** Per-item quotes already applied; omit outer quotes in sentence templates. */
isQuotedList: boolean;
};
export function wrapAuditValue(value: FormattedAuditValue): string {
if (!value.text) return value.text;
return value.isQuotedList ? value.text : `"${value.text}"`;
}
export type AuditActionRuleContext = {
entry: AuditActionSource;
fieldKey: string;
actionKey: string;
oldValue: FormattedAuditValue;
newValue: FormattedAuditValue;
};
export type AuditActionRule = (ctx: AuditActionRuleContext) => string | null;
function formatSeverityAuditLabel(raw: string): string {
const n = Number(raw.trim());
if (n === 1 || n === 2 || n === 3 || n === 4 || n === 5) {
return SEVERITY_LEVELS[n].label;
}
const sevMatch = raw.trim().match(/^sev\s*([1-5])$/i);
if (sevMatch) {
return `SEV ${sevMatch[1]}`;
}
return raw;
}
const CLEARED_AUDIT_VALUE: FormattedAuditValue = { text: "—", isQuotedList: false };
/** `from X to Y` whenever the previous value is known; a cleared value reads as "—". */
function formatFieldChangeSentence(
label: string,
oldValue: FormattedAuditValue,
newValue: FormattedAuditValue,
): string | null {
if (!oldValue.text && !newValue.text) return null;
const next = wrapAuditValue(newValue.text ? newValue : CLEARED_AUDIT_VALUE);
if (oldValue.text) {
return `Changed ${label} from ${wrapAuditValue(oldValue)} to ${next}`;
}
return `Changed ${label} to ${next}`;
}
function tryTypeRule({
fieldKey,
actionKey,
oldValue,
newValue,
}: AuditActionRuleContext): string | null {
const matches =
fieldKey === "type" ||
fieldKey === "workordertype" ||
fieldKey === "wotype" ||
actionKey === "typechanged" ||
actionKey.includes("workordertype");
if (!matches) return null;
return formatFieldChangeSentence("Type", oldValue, newValue);
}
function severityValue(value: FormattedAuditValue): FormattedAuditValue {
return value.text ? { text: formatSeverityAuditLabel(value.text), isQuotedList: false } : value;
}
function trySeverityRule({
fieldKey,
actionKey,
oldValue,
newValue,
}: AuditActionRuleContext): string | null {
const matches = fieldKey.includes("severity") || actionKey.includes("severity");
if (!matches) return null;
return formatFieldChangeSentence("Severity", severityValue(oldValue), severityValue(newValue));
}
function tryStatusRule({
fieldKey,
actionKey,
oldValue,
newValue,
}: AuditActionRuleContext): string | null {
const matches =
fieldKey.includes("status") || fieldKey === "lifecyclestatus" || actionKey.includes("status");
if (!matches || !newValue.text) return null;
if (oldValue.text) {
return `Changed status from ${wrapAuditValue(oldValue)} to ${wrapAuditValue(newValue)}`;
}
return `Changed status to ${wrapAuditValue(newValue)}`;
}
function tryApptRule({
fieldKey,
actionKey,
oldValue,
newValue,
}: AuditActionRuleContext): string | null {
const matches =
fieldKey.includes("appt") || fieldKey.includes("appointment") || actionKey.includes("appt");
if (!matches) return null;
if (oldValue.text && newValue.text) {
return `Changed appt time from ${wrapAuditValue(oldValue)} to ${wrapAuditValue(newValue)}`;
}
if (newValue.text) return `Changed appt time to ${wrapAuditValue(newValue)}`;
return null;
}
function tryScheduleRule({ fieldKey, actionKey, newValue }: AuditActionRuleContext): string | null {
const matches =
fieldKey.includes("scheduled") ||
fieldKey === "schedule" ||
fieldKey.includes("scheduledate") ||
fieldKey.includes("duedate") ||
actionKey === "rescheduled" ||
actionKey.includes("reschedule");
if (!matches) return null;
if (newValue.text) return `Changed schedule to ${wrapAuditValue(newValue)}`;
return "Rescheduled work order";
}
function tryCarriedOverRule({ fieldKey, actionKey, entry }: AuditActionRuleContext): string | null {
const matches =
fieldKey.includes("carriedover") ||
actionKey === "weekrolled" ||
actionKey.includes("carryover");
if (!matches) return null;
const increment = Number(entry.newValue?.trim() ?? "");
if (Number.isFinite(increment) && increment > 0) {
return `Carried over to the next week (increment #${increment})`;
}
return "Carried over to the next week";
}
function tryPastDueRule({ fieldKey, actionKey }: AuditActionRuleContext): string | null {
const matches =
fieldKey.includes("pastdue") || actionKey.includes("pastdue") || actionKey === "markedpastdue";
if (!matches) return null;
return "WO marked as Past Due. Schedule On has passed without completion.";
}
function tryCreatedRule({ fieldKey, actionKey }: AuditActionRuleContext): string | null {
const matches =
fieldKey === "created" || actionKey === "workordercreated" || actionKey === "created";
if (!matches) return null;
return "Work order created in the system.";
}
function tryAssignedToRule({
fieldKey,
actionKey,
newValue,
}: AuditActionRuleContext): string | null {
const matches =
fieldKey === "assignto" ||
fieldKey === "assignedto" ||
fieldKey === "assigneeid" ||
fieldKey === "dispatcherid" ||
actionKey === "assignmentchanged" ||
actionKey.includes("changeassignment");
if (!matches) return null;
const value = newValue.text ? wrapAuditValue(newValue) : '"—"';
return `Changed Assigned To to ${value}`;
}
/** Applied in order; the first non-null result wins. */
export const BOARD_AUDIT_ACTION_RULES: AuditActionRule[] = [
tryStatusRule,
tryTypeRule,
trySeverityRule,
tryApptRule,
tryScheduleRule,
tryCarriedOverRule,
tryPastDueRule,
tryAssignedToRule,
tryCreatedRule,
];