@@ -13,6 +13,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
import { buildBillUpdateAuditDetails } from "../../utils/auditTrailDetails";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
||||||
@@ -134,10 +135,16 @@ export function BillDetailEditcontainer({ insertAuditTrail, bodyshop }) {
|
|||||||
|
|
||||||
await Promise.all(updates);
|
await Promise.all(updates);
|
||||||
|
|
||||||
|
const details = buildBillUpdateAuditDetails({
|
||||||
|
originalBill: data?.bills_by_pk,
|
||||||
|
bill,
|
||||||
|
billlines
|
||||||
|
});
|
||||||
|
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: bill.jobid,
|
jobid: bill.jobid ?? data?.bills_by_pk?.jobid,
|
||||||
billid: search.billid,
|
billid: search.billid,
|
||||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
operation: AuditTrailMapping.billupdated(bill.invoice_number, details),
|
||||||
type: "billupdated"
|
type: "billupdated"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,20 @@ import CriticalPartsScan from "../../utils/criticalPartsScan";
|
|||||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
|
import { buildJobLineInsertAuditDetails, buildJobLineUpdateAuditDetails } from "../../utils/auditTrailDetails.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobLineEditModal: selectJobLineEditModal,
|
jobLineEditModal: selectJobLineEditModal,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit"))
|
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
});
|
});
|
||||||
|
|
||||||
function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop }) {
|
function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bodyshop, insertAuditTrail }) {
|
||||||
const {
|
const {
|
||||||
treatments: { CriticalPartsScanning }
|
treatments: { CriticalPartsScanning }
|
||||||
} = useTreatmentsWithConfig({
|
} = useTreatmentsWithConfig({
|
||||||
@@ -74,6 +78,11 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
|||||||
notification.success({
|
notification.success({
|
||||||
title: t("joblines.successes.created")
|
title: t("joblines.successes.created")
|
||||||
});
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: jobLineEditModal.context.jobid,
|
||||||
|
operation: AuditTrailMapping.jobmanuallineinsert(buildJobLineInsertAuditDetails(values)),
|
||||||
|
type: "jobmanuallineinsert"
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification.error({
|
notification.error({
|
||||||
title: t("joblines.errors.creating", {
|
title: t("joblines.errors.creating", {
|
||||||
@@ -103,6 +112,17 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
|
|||||||
notification.success({
|
notification.success({
|
||||||
title: t("joblines.successes.updated")
|
title: t("joblines.successes.updated")
|
||||||
});
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: jobLineEditModal.context.jobid,
|
||||||
|
operation: AuditTrailMapping.joblineupdate(
|
||||||
|
values.line_desc || jobLineEditModal.context.line_desc || "manual line",
|
||||||
|
buildJobLineUpdateAuditDetails({
|
||||||
|
originalLine: jobLineEditModal.context,
|
||||||
|
values
|
||||||
|
})
|
||||||
|
),
|
||||||
|
type: "joblineupdate"
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("joblines.errors.updating", {
|
title: t("joblines.errors.updating", {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { PageHeader } from "@ant-design/pro-layout";
|
|||||||
import { useMutation, useQuery } from "@apollo/client/react";
|
import { useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Form, Modal, Space } from "antd";
|
import { Button, Form, Modal, Space } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -15,21 +15,27 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
|
||||||
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
import TimeTicketModalComponent from "./time-ticket-modal.component";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions.js";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
import { buildTimeTicketAuditSummary } from "../../utils/auditTrailDetails.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
timeTicketModal: selectTimeTicket,
|
timeTicketModal: selectTimeTicket,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket"))
|
toggleModalVisible: () => dispatch(toggleModalVisible("timeTicket")),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop }) {
|
export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible, bodyshop, insertAuditTrail }) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [enterAgain, setEnterAgain] = useState(false);
|
const [enterAgain, setEnterAgain] = useState(false);
|
||||||
|
|
||||||
|
const lastSubmittedRef = useRef(null);
|
||||||
|
|
||||||
const [lineTicketRefreshKey, setLineTicketRefreshKey] = useState(0);
|
const [lineTicketRefreshKey, setLineTicketRefreshKey] = useState(0);
|
||||||
|
|
||||||
const [insertTicket] = useMutation(INSERT_NEW_TIME_TICKET);
|
const [insertTicket] = useMutation(INSERT_NEW_TIME_TICKET);
|
||||||
@@ -48,47 +54,77 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
const employees = EmployeeAutoCompleteData?.employees ?? [];
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = (values) => {
|
||||||
|
lastSubmittedRef.current = values;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const emps = EmployeeAutoCompleteData?.employees.filter((e) => e.id === values.employeeid);
|
const isEdit = Boolean(timeTicketModal.context.id);
|
||||||
if (timeTicketModal.context.id) {
|
const emps = employees.filter((employee) => employee.id === values.employeeid);
|
||||||
updateTicket({
|
const mutation = isEdit
|
||||||
variables: {
|
? updateTicket({
|
||||||
timeticketId: timeTicketModal.context.id,
|
variables: {
|
||||||
timeticket: {
|
timeticketId: timeTicketModal.context.id,
|
||||||
...values,
|
timeticket: {
|
||||||
rate: emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0]?.rate : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(handleMutationSuccess)
|
|
||||||
.catch(handleMutationError);
|
|
||||||
} else {
|
|
||||||
//Get selected employee rate.
|
|
||||||
insertTicket({
|
|
||||||
variables: {
|
|
||||||
timeTicketInput: [
|
|
||||||
{
|
|
||||||
...values,
|
...values,
|
||||||
rate:
|
rate:
|
||||||
emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0].rate : null,
|
emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0]?.rate : null
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
created_by: timeTicketModal.context.created_by
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
}
|
})
|
||||||
})
|
: insertTicket({
|
||||||
.then(handleMutationSuccess)
|
variables: {
|
||||||
.catch(handleMutationError);
|
timeTicketInput: [
|
||||||
}
|
{
|
||||||
|
...values,
|
||||||
|
rate:
|
||||||
|
emps.length === 1 ? emps[0].rates.filter((r) => r.cost_center === values.cost_center)[0].rate : null,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
created_by: timeTicketModal.context.created_by
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mutation.then((result) => handleMutationSuccess(result, isEdit)).catch(handleMutationError);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMutationSuccess = () => {
|
const handleMutationSuccess = (result, isEdit) => {
|
||||||
notification.success({
|
notification.success({
|
||||||
title: t("timetickets.successes.created")
|
title: t("timetickets.successes.created")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const savedTicket =
|
||||||
|
result?.data?.update_timetickets?.returning?.[0] ?? result?.data?.insert_timetickets?.returning?.[0] ?? {};
|
||||||
|
const originalTicket = timeTicketModal.context?.timeticket ?? {};
|
||||||
|
const submittedValues = {
|
||||||
|
...(lastSubmittedRef.current ?? {}),
|
||||||
|
date: lastSubmittedRef.current?.date ?? savedTicket.date ?? originalTicket.date ?? null,
|
||||||
|
employeeid: lastSubmittedRef.current?.employeeid ?? savedTicket.employeeid ?? originalTicket.employeeid ?? null,
|
||||||
|
jobid:
|
||||||
|
lastSubmittedRef.current?.jobid ??
|
||||||
|
savedTicket.jobid ??
|
||||||
|
timeTicketModal.context.jobId ??
|
||||||
|
originalTicket.job?.id ??
|
||||||
|
originalTicket.jobid ??
|
||||||
|
null
|
||||||
|
};
|
||||||
|
const auditSummary = buildTimeTicketAuditSummary({
|
||||||
|
originalTicket,
|
||||||
|
submittedValues,
|
||||||
|
employees
|
||||||
|
});
|
||||||
|
|
||||||
|
if (auditSummary.jobid) {
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: auditSummary.jobid,
|
||||||
|
operation: isEdit
|
||||||
|
? AuditTrailMapping.timeticketupdated(auditSummary.employeeName, auditSummary.date, auditSummary.details)
|
||||||
|
: AuditTrailMapping.timeticketcreated(auditSummary.employeeName, auditSummary.date, auditSummary.details),
|
||||||
|
type: isEdit ? "timeticketupdated" : "timeticketcreated"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh parent screens (Job Labor tab, etc.)
|
// Refresh parent screens (Job Labor tab, etc.)
|
||||||
if (timeTicketModal.actions.refetch) timeTicketModal.actions.refetch();
|
if (timeTicketModal.actions.refetch) timeTicketModal.actions.refetch();
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||||
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||||
import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries";
|
import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries";
|
||||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import JobsCreateComponent from "./jobs-create.component";
|
import JobsCreateComponent from "./jobs-create.component";
|
||||||
import JobCreateContext from "./jobs-create.context";
|
import JobCreateContext from "./jobs-create.context";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -22,10 +23,11 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
});
|
});
|
||||||
|
|
||||||
function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser }) {
|
function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, currentUser, insertAuditTrail }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
@@ -84,6 +86,11 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr
|
|||||||
newJobId: resp.data.insert_jobs.returning[0].id
|
newJobId: resp.data.insert_jobs.returning[0].id
|
||||||
});
|
});
|
||||||
logImEXEvent("manual_job_create_completed", {});
|
logImEXEvent("manual_job_create_completed", {});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: resp.data.insert_jobs.returning[0].id,
|
||||||
|
operation: AuditTrailMapping.jobmanualcreate(),
|
||||||
|
type: "jobmanualcreate"
|
||||||
|
});
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
|
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
|
||||||
"billmarkforreexport": "Bill with invoice number {{invoice_number}} marked for re-export.",
|
"billmarkforreexport": "Bill with invoice number {{invoice_number}} marked for re-export.",
|
||||||
"billposted": "Bill with invoice number {{invoice_number}} posted.",
|
"billposted": "Bill with invoice number {{invoice_number}} posted.",
|
||||||
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
|
"billupdated": "Bill with invoice number {{invoice_number}} updated with the following details: {{details}}.",
|
||||||
"failedpayment": "Failed payment attempt.",
|
"failedpayment": "Failed payment attempt.",
|
||||||
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
|
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
|
||||||
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
|
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
|
||||||
@@ -137,6 +137,9 @@
|
|||||||
"jobintake": "Job intake completed. Status set to {{status}}. Scheduled completion is {{scheduled_completion}}.",
|
"jobintake": "Job intake completed. Status set to {{status}}. Scheduled completion is {{scheduled_completion}}.",
|
||||||
"jobinvoiced": "Job has been invoiced.",
|
"jobinvoiced": "Job has been invoiced.",
|
||||||
"jobioucreated": "IOU Created.",
|
"jobioucreated": "IOU Created.",
|
||||||
|
"joblineupdate": "Job line {{lineDescription}} updated with the following details: {{details}}.",
|
||||||
|
"jobmanualcreate": "Job manually created.",
|
||||||
|
"jobmanuallineinsert": "Job line manually added with the following details: {{details}}.",
|
||||||
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
|
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
|
||||||
"jobnoteadded": "Note added to Job.",
|
"jobnoteadded": "Note added to Job.",
|
||||||
"jobnotedeleted": "Note deleted from Job.",
|
"jobnotedeleted": "Note deleted from Job.",
|
||||||
@@ -152,7 +155,9 @@
|
|||||||
"tasks_deleted": "Task '{{title}}' deleted by {{deletedBy}}",
|
"tasks_deleted": "Task '{{title}}' deleted by {{deletedBy}}",
|
||||||
"tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}",
|
"tasks_uncompleted": "Task '{{title}}' uncompleted by {{uncompletedBy}}",
|
||||||
"tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}",
|
"tasks_undeleted": "Task '{{title}}' undeleted by {{undeletedBy}}",
|
||||||
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}"
|
"tasks_updated": "Task '{{title}}' updated by {{updatedBy}}",
|
||||||
|
"timeticketcreated": "Time Ticket for {{employee}} on {{date}} created with the following details: {{details}}.",
|
||||||
|
"timeticketupdated": "Time Ticket for {{employee}} on {{date}} updated with the following details: {{details}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"billlines": {
|
"billlines": {
|
||||||
|
|||||||
@@ -122,7 +122,6 @@
|
|||||||
"billdeleted": "",
|
"billdeleted": "",
|
||||||
"billposted": "",
|
"billposted": "",
|
||||||
"billmarkforreexport": "",
|
"billmarkforreexport": "",
|
||||||
"billupdated": "",
|
|
||||||
"failedpayment": "",
|
"failedpayment": "",
|
||||||
"jobassignmentchange": "",
|
"jobassignmentchange": "",
|
||||||
"jobassignmentremoved": "",
|
"jobassignmentremoved": "",
|
||||||
|
|||||||
@@ -122,7 +122,6 @@
|
|||||||
"billdeleted": "",
|
"billdeleted": "",
|
||||||
"billmarkforreexport": "",
|
"billmarkforreexport": "",
|
||||||
"billposted": "",
|
"billposted": "",
|
||||||
"billupdated": "",
|
|
||||||
"failedpayment": "",
|
"failedpayment": "",
|
||||||
"jobassignmentchange": "",
|
"jobassignmentchange": "",
|
||||||
"jobassignmentremoved": "",
|
"jobassignmentremoved": "",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const AuditTrailMapping = {
|
|||||||
billdeleted: (invoice_number) => i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
|
billdeleted: (invoice_number) => i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
|
||||||
billmarkforreexport: (invoice_number) => i18n.t("audit_trail.messages.billmarkforreexport", { invoice_number }),
|
billmarkforreexport: (invoice_number) => i18n.t("audit_trail.messages.billmarkforreexport", { invoice_number }),
|
||||||
billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }),
|
billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }),
|
||||||
billupdated: (invoice_number) => i18n.t("audit_trail.messages.billupdated", { invoice_number }),
|
billupdated: (invoice_number, details) => i18n.t("audit_trail.messages.billupdated", { invoice_number, details }),
|
||||||
jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }),
|
jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }),
|
||||||
jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }),
|
jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }),
|
||||||
jobchecklist: (type, inproduction, status) =>
|
jobchecklist: (type, inproduction, status) =>
|
||||||
@@ -26,6 +26,10 @@ const AuditTrailMapping = {
|
|||||||
jobinproductionchange: (inproduction) => i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
|
jobinproductionchange: (inproduction) => i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }),
|
||||||
jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"),
|
jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"),
|
||||||
jobclosedwithbypass: () => i18n.t("audit_trail.messages.jobclosedwithbypass"),
|
jobclosedwithbypass: () => i18n.t("audit_trail.messages.jobclosedwithbypass"),
|
||||||
|
joblineupdate: (lineDescription, details) =>
|
||||||
|
i18n.t("audit_trail.messages.joblineupdate", { details, lineDescription }),
|
||||||
|
jobmanualcreate: () => i18n.t("audit_trail.messages.jobmanualcreate"),
|
||||||
|
jobmanuallineinsert: (details) => i18n.t("audit_trail.messages.jobmanuallineinsert", { details }),
|
||||||
jobmodifylbradj: ({ mod_lbr_ty, hours }) => i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
|
jobmodifylbradj: ({ mod_lbr_ty, hours }) => i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
|
||||||
jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"),
|
jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"),
|
||||||
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
|
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
|
||||||
@@ -72,7 +76,11 @@ const AuditTrailMapping = {
|
|||||||
i18n.t("audit_trail.messages.tasks_uncompleted", {
|
i18n.t("audit_trail.messages.tasks_uncompleted", {
|
||||||
title,
|
title,
|
||||||
uncompletedBy
|
uncompletedBy
|
||||||
})
|
}),
|
||||||
|
timeticketcreated: (employee, date, details) =>
|
||||||
|
i18n.t("audit_trail.messages.timeticketcreated", { employee, date, details }),
|
||||||
|
timeticketupdated: (employee, date, details) =>
|
||||||
|
i18n.t("audit_trail.messages.timeticketupdated", { employee, date, details })
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuditTrailMapping;
|
export default AuditTrailMapping;
|
||||||
|
|||||||
186
client/src/utils/auditTrailDetails.js
Normal file
186
client/src/utils/auditTrailDetails.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user