shoc-frontend-new/src/domain/work-orders/utils/format-board-audit-action.ts

179 lines
5.5 KiB
TypeScript
Raw Normal View History

import type { WorkOrderAuditEntry } from "@/domain/work-orders/types/work-order-detail";
type AuditActionSource = Pick<
WorkOrderAuditEntry,
"action" | "fieldName" | "oldValue" | "newValue"
>;
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;
}
}
type FormattedAuditValue = {
text: string;
/** Per-item quotes already applied; omit outer quotes in sentence templates. */
isQuotedList: boolean;
};
function wrapAuditValue(value: FormattedAuditValue): string {
if (!value.text) return value.text;
return value.isQuotedList ? value.text : `"${value.text}"`;
}
/** 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);
if (
fieldKey.includes("status") ||
fieldKey === "lifecyclestatus" ||
actionKey.includes("status")
) {
if (newValue.text) return `Changed status to ${wrapAuditValue(newValue)}`;
}
if (fieldKey.includes("appt") || fieldKey.includes("appointment") || actionKey.includes("appt")) {
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)}`;
}
if (
fieldKey.includes("scheduled") ||
fieldKey === "schedule" ||
fieldKey.includes("scheduledate") ||
fieldKey.includes("duedate") ||
actionKey === "rescheduled" ||
actionKey.includes("reschedule")
) {
if (newValue.text) return `Changed schedule to ${wrapAuditValue(newValue)}`;
return "Rescheduled work order";
}
if (
fieldKey.includes("carriedover") ||
actionKey === "weekrolled" ||
actionKey.includes("carryover")
) {
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";
}
if (
fieldKey.includes("pastdue") ||
actionKey.includes("pastdue") ||
actionKey === "markedpastdue"
) {
return "WO marked as Past Due. Scheduled date has passed without completion.";
}
if (fieldKey === "created" || actionKey === "workordercreated" || actionKey === "created") {
return "Work order created in the system.";
}
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)}`;
}
if (action) return action;
return "Updated work order";
}