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, ];