2026-07-28 09:56:27 -03:00
|
|
|
|
import {
|
|
|
|
|
|
BOARD_AUDIT_ACTION_RULES,
|
|
|
|
|
|
wrapAuditValue,
|
|
|
|
|
|
type AuditActionSource,
|
|
|
|
|
|
type FormattedAuditValue,
|
|
|
|
|
|
} from "@/domain/work-orders/utils/board-audit-action-rules";
|
2026-07-17 10:45:59 -03:00
|
|
|
|
|
|
|
|
|
|
function looksLikeHumanSentence(action: string): boolean {
|
|
|
|
|
|
return /\s/.test(action);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function humanizeFieldLabel(fieldName: string): string {
|
|
|
|
|
|
const spaced = fieldName
|
|
|
|
|
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
|
|
|
|
.replace(/[_-]+/g, " ")
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.toLowerCase();
|
|
|
|
|
|
return spaced || "field";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeKey(value: string): string {
|
|
|
|
|
|
return value
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
|
.replace(/[\s_-]+/g, "");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** JSON array strings → `"a", "b", "c"` instead of `["a","b","c"]`. */
|
|
|
|
|
|
function formatJsonArrayDisplayValue(raw: string): string | null {
|
|
|
|
|
|
const trimmed = raw.trim();
|
|
|
|
|
|
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed: unknown = JSON.parse(trimmed);
|
|
|
|
|
|
if (!Array.isArray(parsed)) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const items = parsed
|
|
|
|
|
|
.map((item) => String(item ?? "").trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.map((item) => `"${item}"`);
|
|
|
|
|
|
|
|
|
|
|
|
return items.join(", ");
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Parse ISO / date-only / pipe-separated API values → unique mm/dd/yyyy. */
|
|
|
|
|
|
function formatAuditDisplayValue(raw: string): FormattedAuditValue {
|
|
|
|
|
|
const trimmed = raw.trim();
|
|
|
|
|
|
if (!trimmed) return { text: trimmed, isQuotedList: false };
|
|
|
|
|
|
|
|
|
|
|
|
const jsonArray = formatJsonArrayDisplayValue(trimmed);
|
|
|
|
|
|
if (jsonArray !== null) return { text: jsonArray, isQuotedList: true };
|
|
|
|
|
|
|
|
|
|
|
|
const parts = trimmed
|
|
|
|
|
|
.split("|")
|
|
|
|
|
|
.map((part) => part.trim())
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
|
|
const dates: string[] = [];
|
|
|
|
|
|
for (const part of parts) {
|
|
|
|
|
|
const isoMatch = part.match(/^(\d{4})-(\d{2})-(\d{2})(?:T|$)/);
|
|
|
|
|
|
if (isoMatch) {
|
|
|
|
|
|
const [, year, month, day] = isoMatch;
|
|
|
|
|
|
dates.push(`${month}/${day}/${year}`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
const date = new Date(part);
|
|
|
|
|
|
if (!Number.isNaN(date.getTime()) && /\d{4}/.test(part)) {
|
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
|
|
|
|
const year = String(date.getFullYear());
|
|
|
|
|
|
dates.push(`${month}/${day}/${year}`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
return { text: trimmed, isQuotedList: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const unique = [...new Set(dates)];
|
|
|
|
|
|
if (unique.length === 0) return { text: trimmed, isQuotedList: false };
|
|
|
|
|
|
if (unique.length === 1) return { text: unique[0]!, isQuotedList: false };
|
|
|
|
|
|
return { text: unique.join(" – "), isQuotedList: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Builds a design-style audit sentence (the part after `Name · `),
|
|
|
|
|
|
* e.g. `Changed status to "Incomplete"`, from board API fields.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function formatBoardAuditAction(entry: AuditActionSource): string {
|
|
|
|
|
|
const action = entry.action?.trim() ?? "";
|
|
|
|
|
|
const fieldName = entry.fieldName?.trim() ?? "";
|
|
|
|
|
|
const oldValue = formatAuditDisplayValue(entry.oldValue?.trim() ?? "");
|
|
|
|
|
|
const newValue = formatAuditDisplayValue(entry.newValue?.trim() ?? "");
|
|
|
|
|
|
|
|
|
|
|
|
if (action && looksLikeHumanSentence(action)) {
|
|
|
|
|
|
return action;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fieldKey = normalizeKey(fieldName);
|
|
|
|
|
|
const actionKey = normalizeKey(action);
|
|
|
|
|
|
|
2026-07-28 09:56:27 -03:00
|
|
|
|
const ruleContext = { entry, fieldKey, actionKey, oldValue, newValue };
|
|
|
|
|
|
for (const rule of BOARD_AUDIT_ACTION_RULES) {
|
|
|
|
|
|
const result = rule(ruleContext);
|
|
|
|
|
|
if (result !== null) return result;
|
2026-07-17 10:45:59 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 09:56:27 -03:00
|
|
|
|
return formatGenericAuditFallback({ action, fieldName, oldValue, newValue });
|
|
|
|
|
|
}
|
2026-07-17 10:45:59 -03:00
|
|
|
|
|
2026-07-28 09:56:27 -03:00
|
|
|
|
function formatGenericAuditFallback(args: {
|
|
|
|
|
|
action: string;
|
|
|
|
|
|
fieldName: string;
|
|
|
|
|
|
oldValue: FormattedAuditValue;
|
|
|
|
|
|
newValue: FormattedAuditValue;
|
|
|
|
|
|
}): string {
|
|
|
|
|
|
const { action, fieldName, oldValue, newValue } = args;
|
2026-07-17 10:45:59 -03:00
|
|
|
|
if (!action && !fieldName && !oldValue.text && !newValue.text) {
|
|
|
|
|
|
return "Work order created in the system.";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const label = humanizeFieldLabel(fieldName || action || "field");
|
|
|
|
|
|
if (oldValue.text && newValue.text) {
|
|
|
|
|
|
return `Changed ${label} from ${wrapAuditValue(oldValue)} to ${wrapAuditValue(newValue)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (newValue.text) {
|
|
|
|
|
|
return `Changed ${label} to ${wrapAuditValue(newValue)}`;
|
|
|
|
|
|
}
|
2026-09-16 11:43:34 -03:00
|
|
|
|
if (oldValue.text) {
|
|
|
|
|
|
return `Changed ${label} from ${wrapAuditValue(oldValue)} to "—"`;
|
|
|
|
|
|
}
|
2026-09-16 16:56:26 -03:00
|
|
|
|
// Never surface a raw audit enum such as "FieldChanged" as the sentence.
|
2026-09-16 11:43:34 -03:00
|
|
|
|
if (fieldName && action) return `Updated ${label}`;
|
2026-07-17 10:45:59 -03:00
|
|
|
|
return "Updated work order";
|
|
|
|
|
|
}
|