187 lines
7.0 KiB
JavaScript
187 lines
7.0 KiB
JavaScript
import dayjs from "./day";
|
|
|
|
const EMPTY_VALUE = "<<empty>>";
|
|
const NO_CHANGES = "No changes";
|
|
|
|
const BILL_LINE_KEYS = ["line_desc", "quantity", "actual_price", "actual_cost", "cost_center"];
|
|
const JOB_LINE_SKIP_KEYS = new Set(["ah_detail_line", "prt_dsmk_p"]);
|
|
const DATE_ONLY_KEYS = new Set(["date"]);
|
|
const DATE_TIME_KEYS = new Set(["clockon", "clockoff"]);
|
|
const CURRENCY_KEYS = new Set(["actual_price", "actual_cost", "act_price", "db_price", "rate"]);
|
|
const HOUR_KEYS = new Set(["productivehrs", "actualhrs", "mod_lb_hrs"]);
|
|
|
|
const isBlank = (value) => value == null || value === "";
|
|
|
|
const isStructuredValue = (value) => value != null && typeof value === "object" && !dayjs.isDayjs?.(value);
|
|
|
|
const formatDate = (value) => (isBlank(value) ? EMPTY_VALUE : dayjs(value).format("YYYY-MM-DD"));
|
|
|
|
const formatDateTime = (value) => (isBlank(value) ? EMPTY_VALUE : dayjs(value).format("YYYY-MM-DD HH:mm"));
|
|
|
|
const formatNumber = (value, fractionDigits) =>
|
|
typeof value === "number" ? value.toFixed(fractionDigits) : String(value);
|
|
|
|
const compareValue = (key, value) => {
|
|
if (isBlank(value)) return EMPTY_VALUE;
|
|
if (DATE_TIME_KEYS.has(key)) return formatDateTime(value);
|
|
if (DATE_ONLY_KEYS.has(key)) return formatDate(value);
|
|
if (dayjs.isDayjs?.(value)) return formatDateTime(value);
|
|
return String(value);
|
|
};
|
|
|
|
const buildFieldChangeDetails = ({ keys, original = {}, updated = {}, displayValue, skippedKeys = new Set() }) =>
|
|
keys
|
|
.filter((key) => key !== "__typename" && !skippedKeys.has(key))
|
|
.filter((key) => !isStructuredValue(original[key]) && !isStructuredValue(updated[key]))
|
|
.map((key) => {
|
|
if (compareValue(key, original[key]) === compareValue(key, updated[key])) return null;
|
|
return `${key}: ${displayValue(key, original[key])} -> ${displayValue(key, updated[key])}`;
|
|
})
|
|
.filter(Boolean);
|
|
|
|
const formatBillValue = (key, value) => {
|
|
if (isBlank(value)) return EMPTY_VALUE;
|
|
if (DATE_ONLY_KEYS.has(key)) return formatDate(value);
|
|
if (CURRENCY_KEYS.has(key)) return typeof value === "number" ? `$${value.toFixed(2)}` : String(value);
|
|
return String(value);
|
|
};
|
|
|
|
const formatJobLineValue = (key, value) => {
|
|
if (isBlank(value)) return EMPTY_VALUE;
|
|
if (CURRENCY_KEYS.has(key)) return typeof value === "number" ? `$${value.toFixed(2)}` : String(value);
|
|
if (HOUR_KEYS.has(key)) return formatNumber(value, 1);
|
|
return String(value);
|
|
};
|
|
|
|
const getEmployeeName = (employeeId, employees = [], fallbackEmployee) => {
|
|
if (
|
|
(employeeId == null || fallbackEmployee?.id === employeeId) &&
|
|
(fallbackEmployee?.first_name || fallbackEmployee?.last_name)
|
|
) {
|
|
return [fallbackEmployee.first_name, fallbackEmployee.last_name].filter(Boolean).join(" ");
|
|
}
|
|
|
|
const employee = employees.find(({ id }) => id === employeeId);
|
|
if (employee) {
|
|
return [employee.first_name, employee.last_name].filter(Boolean).join(" ");
|
|
}
|
|
|
|
return employeeId ? String(employeeId) : EMPTY_VALUE;
|
|
};
|
|
|
|
const formatTimeTicketValue = (key, value, { employees = [], fallbackEmployee } = {}) => {
|
|
if (isBlank(value)) return EMPTY_VALUE;
|
|
if (key === "employeeid") return getEmployeeName(value, employees, fallbackEmployee);
|
|
if (DATE_TIME_KEYS.has(key)) return formatDateTime(value);
|
|
if (DATE_ONLY_KEYS.has(key)) return formatDate(value);
|
|
if (CURRENCY_KEYS.has(key)) return typeof value === "number" ? `$${value.toFixed(2)}` : String(value);
|
|
if (HOUR_KEYS.has(key)) return formatNumber(value, 1);
|
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
return String(value);
|
|
};
|
|
|
|
const buildBillLineSummary = (line) =>
|
|
BILL_LINE_KEYS.map((key) => `${key}: ${formatBillValue(key, line[key])}`).join(", ");
|
|
|
|
export function buildBillUpdateAuditDetails({ originalBill = {}, bill = {}, billlines = [] }) {
|
|
const updatedBill = { ...bill, billlines };
|
|
const billKeys = Array.from(new Set([...Object.keys(originalBill), ...Object.keys(updatedBill)])).filter(
|
|
(key) => key !== "billlines"
|
|
);
|
|
|
|
const changed = buildFieldChangeDetails({
|
|
keys: billKeys,
|
|
original: originalBill,
|
|
updated: updatedBill,
|
|
displayValue: formatBillValue
|
|
});
|
|
|
|
const originalBillLines = originalBill.billlines ?? [];
|
|
const updatedBillLines = updatedBill.billlines ?? [];
|
|
|
|
const addedLines = updatedBillLines
|
|
.filter((line) => !line.id)
|
|
.map((line) => `+${line.line_desc || line.description || "new line"} (${buildBillLineSummary(line)})`);
|
|
|
|
const removedLines = originalBillLines
|
|
.filter((line) => !updatedBillLines.some((updatedLine) => updatedLine.id === line.id))
|
|
.map(
|
|
(line) => `-${line.line_desc || line.description || line.id || "removed line"} (${buildBillLineSummary(line)})`
|
|
);
|
|
|
|
const modifiedLines = updatedBillLines
|
|
.filter((line) => line.id)
|
|
.flatMap((line) => {
|
|
const originalLine = originalBillLines.find(({ id }) => id === line.id);
|
|
if (!originalLine) return [];
|
|
|
|
const lineChanges = buildFieldChangeDetails({
|
|
keys: BILL_LINE_KEYS,
|
|
original: originalLine,
|
|
updated: line,
|
|
displayValue: formatBillValue
|
|
});
|
|
|
|
if (!lineChanges.length) return [];
|
|
|
|
return [`${line.line_desc || line.description || line.id}: ${lineChanges.join("; ")}`];
|
|
});
|
|
|
|
if (addedLines.length) changed.push(`billlines added: ${addedLines.join(" | ")}`);
|
|
if (removedLines.length) changed.push(`billlines removed: ${removedLines.join(" | ")}`);
|
|
if (modifiedLines.length) changed.push(`billlines modified: ${modifiedLines.join(" | ")}`);
|
|
|
|
return changed.length ? changed.join("; ") : NO_CHANGES;
|
|
}
|
|
|
|
export function buildJobLineInsertAuditDetails(values = {}) {
|
|
const details = Object.entries(values)
|
|
.filter(([key, value]) => !JOB_LINE_SKIP_KEYS.has(key) && !isBlank(value))
|
|
.map(([key, value]) => `${key}: ${formatJobLineValue(key, value)}`);
|
|
|
|
return details.length ? details.join("; ") : NO_CHANGES;
|
|
}
|
|
|
|
export function buildJobLineUpdateAuditDetails({ originalLine = {}, values = {} }) {
|
|
const details = buildFieldChangeDetails({
|
|
keys: Object.keys(values),
|
|
original: originalLine,
|
|
updated: values,
|
|
displayValue: formatJobLineValue,
|
|
skippedKeys: JOB_LINE_SKIP_KEYS
|
|
});
|
|
|
|
return details.length ? details.join("; ") : NO_CHANGES;
|
|
}
|
|
|
|
export function buildTimeTicketAuditSummary({ originalTicket = {}, submittedValues = {}, employees = [] }) {
|
|
const normalizedOriginal = {
|
|
...originalTicket,
|
|
jobid: originalTicket.job?.id ?? originalTicket.jobid ?? null
|
|
};
|
|
|
|
const details = buildFieldChangeDetails({
|
|
keys: Object.keys(submittedValues),
|
|
original: normalizedOriginal,
|
|
updated: submittedValues,
|
|
displayValue: (key, value) =>
|
|
formatTimeTicketValue(key, value, {
|
|
employees,
|
|
fallbackEmployee: key === "employeeid" ? normalizedOriginal.employee : null
|
|
})
|
|
});
|
|
|
|
const employeeName = getEmployeeName(
|
|
submittedValues.employeeid ?? normalizedOriginal.employeeid,
|
|
employees,
|
|
normalizedOriginal.employee
|
|
);
|
|
|
|
return {
|
|
date: formatDate(submittedValues.date ?? normalizedOriginal.date),
|
|
details: details.length ? details.join("; ") : NO_CHANGES,
|
|
employeeName,
|
|
jobid: submittedValues.jobid ?? normalizedOriginal.jobid ?? null
|
|
};
|
|
}
|