mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 01:52:04 +00:00
179 lines
5.5 KiB
TypeScript
179 lines
5.5 KiB
TypeScript
|
|
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";
|
|||
|
|
}
|