Merged in feature/IO-1366-Audit-Logging (pull request #1311)

Feature/IO-1366 Audit Logging

Approved-by: Dave Richer
This commit is contained in:
Allan Carr
2024-03-01 02:44:05 +00:00
committed by Dave Richer
11 changed files with 122 additions and 15 deletions

View File

@@ -3,10 +3,22 @@ import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd"; import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { DELETE_BILL } from "../../graphql/bills.queries"; import { DELETE_BILL } from "../../graphql/bills.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
export default function BillDeleteButton({ bill, callback }) { const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL); const [deleteBill] = useMutation(DELETE_BILL);
@@ -36,6 +48,10 @@ export default function BillDeleteButton({ bill, callback }) {
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") }); notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
});
if (callback && typeof callback === "function") callback(bill.id); if (callback && typeof callback === "function") callback(bill.id);
} else { } else {

View File

@@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort, dateSort } from "../../utils/sorters";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -58,7 +58,7 @@ export function BillsListTableComponent({
<EditFilled /> <EditFilled />
</Button> </Button>
)} )}
<BillDeleteButton bill={record} /> <BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent <BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }} data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={ disabled={

View File

@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient"; import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
function updateJobCache(items) { function updateJobCache(items) {
client.cache.modify({ client.cache.modify({
id: "ROOT_QUERY", id: "ROOT_QUERY",
@@ -40,6 +47,7 @@ export function JobsCloseExportButton({
disabled, disabled,
setSelectedJobs, setSelectedJobs,
refetch, refetch,
insertAuditTrail,
}) { }) {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -181,6 +189,10 @@ export function JobsCloseExportButton({
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
});
updateJobCache( updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id) jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
); );
@@ -192,12 +204,20 @@ export function JobsCloseExportButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
});
updateJobCache([ updateJobCache([
...new Set( ...new Set(
successfulTransactions.map( successfulTransactions.map(
@@ -227,4 +247,7 @@ export function JobsCloseExportButton({
); );
} }
export default connect(mapStateToProps, null)(JobsCloseExportButton); export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsCloseExportButton);

View File

@@ -129,6 +129,12 @@ export function JobsDetailHeaderActions({
}, },
}, },
}); });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobsuspend(
!!job.suspended ? !job.suspended : true
),
});
}; };
const statusmenu = ( const statusmenu = (
@@ -540,6 +546,10 @@ export function JobsDetailHeaderActions({
notification["success"]({ notification["success"]({
message: t("jobs.successes.voided"), message: t("jobs.successes.voided"),
}); });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobvoid(),
});
//go back to jobs list. //go back to jobs list.
history.push(`/manage/`); history.push(`/manage/`);
} else { } else {

View File

@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOBS } from "../../graphql/jobs.queries"; import { UPDATE_JOBS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient"; import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
function updateJobCache(items) { function updateJobCache(items) {
client.cache.modify({ client.cache.modify({
id: "ROOT_QUERY", id: "ROOT_QUERY",
@@ -41,6 +48,7 @@ export function JobsExportAllButton({
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch, refetch,
insertAuditTrail,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS); const [updateJob] = useMutation(UPDATE_JOBS);
@@ -177,6 +185,12 @@ export function JobsExportAllButton({
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobexported(),
});
});
updateJobCache( updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map( jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id (job) => job.id
@@ -190,13 +204,17 @@ export function JobsExportAllButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
updateJobCache([ const successfulTransactionsSet = [
...new Set( ...new Set(
successfulTransactions.map( successfulTransactions.map(
(st) => (st) =>
@@ -207,7 +225,14 @@ export function JobsExportAllButton({
] ]
) )
), ),
]); ];
if (successfulTransactionsSet.length > 0) {
insertAuditTrail({
jobid: successfulTransactionsSet[0],
operation: AuditTrailMapping.jobexported(),
});
}
updateJobCache(successfulTransactionsSet);
} }
} }
}) })
@@ -225,4 +250,7 @@ export function JobsExportAllButton({
); );
} }
export default connect(mapStateToProps, null)(JobsExportAllButton); export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsExportAllButton);

View File

@@ -26,10 +26,12 @@ import { OwnerNameDisplayFunction } from "../../components/owner-name-display/ow
import { auth } from "../../firebase/firebase.utils"; import { auth } from "../../firebase/firebase.utils";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries"; import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import { import {
insertAuditTrail,
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -38,6 +40,8 @@ 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 }) =>
dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
@@ -46,7 +50,7 @@ export const socket = SocketIO(
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
? process.env.REACT_APP_AXIOS_BASE_API_URL ? process.env.REACT_APP_AXIOS_BASE_API_URL
: window.location.origin, : window.location.origin,
// "http://localhost:4000", // for dev testing, // "http://localhost:4000", // for dev testing,
{ {
path: "/ws", path: "/ws",
withCredentials: true, withCredentials: true,
@@ -57,7 +61,12 @@ export const socket = SocketIO(
} }
); );
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { export function DmsContainer({
bodyshop,
setBreadcrumbs,
setSelectedHeader,
insertAuditTrail,
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG"); const [logLevel, setLogLevel] = useState("DEBUG");
const history = useHistory(); const history = useHistory();
@@ -115,6 +124,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
notification.success({ notification.success({
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
});
history.push("/manage/accounting/receivables"); history.push("/manage/accounting/receivables");
}); });

View File

@@ -289,7 +289,7 @@ export function* insertAuditTrailSaga({
fields: { fields: {
audit_trail(existingAuditTrail, { readField }) { audit_trail(existingAuditTrail, { readField }) {
const newAuditTrail = cache.writeQuery({ const newAuditTrail = cache.writeQuery({
data: data.insert_audit_trail_one, data: data,
query: INSERT_AUDIT_TRAIL, query: INSERT_AUDIT_TRAIL,
variables, variables,
}); });

View File

@@ -107,6 +107,7 @@
"alerttoggle": "Alert Toggle set to {{status}}", "alerttoggle": "Alert Toggle set to {{status}}",
"appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.", "appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.",
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.", "appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
"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.",
"failedpayment": "Failed payment", "failedpayment": "Failed payment",
@@ -114,6 +115,7 @@
"jobassignmentremoved": "Employee assignment removed for {{operation}}", "jobassignmentremoved": "Employee assignment removed for {{operation}}",
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.", "jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
"jobconverted": "Job converted and assigned number {{ro_number}}.", "jobconverted": "Job converted and assigned number {{ro_number}}.",
"jobexported": "Job has been exported.",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.", "jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.", "jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}", "jobinproductionchange": "Job production status set to {{inproduction}}",
@@ -126,7 +128,9 @@
"jobspartsorder": "Parts order {{order_number}} added to Job.", "jobspartsorder": "Parts order {{order_number}} added to Job.",
"jobspartsreturn": "Parts return {{order_number}} added to Job.", "jobspartsreturn": "Parts return {{order_number}} added to Job.",
"jobstatuschange": "Job status changed to {{status}}.", "jobstatuschange": "Job status changed to {{status}}.",
"jobsupplement": "Job supplement imported." "jobsupplement": "Job supplement imported.",
"jobsuspend": "Suspend Toggle set to {{status}}",
"jobvoid": "Job has been voided."
} }
}, },
"billlines": { "billlines": {

View File

@@ -107,6 +107,7 @@
"alerttoggle": "", "alerttoggle": "",
"appointmentcancel": "", "appointmentcancel": "",
"appointmentinsert": "", "appointmentinsert": "",
"billdeleted": "",
"billposted": "", "billposted": "",
"billupdated": "", "billupdated": "",
"failedpayment": "", "failedpayment": "",
@@ -114,6 +115,7 @@
"jobassignmentremoved": "", "jobassignmentremoved": "",
"jobchecklist": "", "jobchecklist": "",
"jobconverted": "", "jobconverted": "",
"jobexported": "",
"jobfieldchanged": "", "jobfieldchanged": "",
"jobimported": "", "jobimported": "",
"jobinproductionchange": "", "jobinproductionchange": "",
@@ -126,7 +128,9 @@
"jobspartsorder": "", "jobspartsorder": "",
"jobspartsreturn": "", "jobspartsreturn": "",
"jobstatuschange": "", "jobstatuschange": "",
"jobsupplement": "" "jobsupplement": "",
"jobsuspend": "",
"jobvoid": ""
} }
}, },
"billlines": { "billlines": {

View File

@@ -107,6 +107,7 @@
"alerttoggle": "", "alerttoggle": "",
"appointmentcancel": "", "appointmentcancel": "",
"appointmentinsert": "", "appointmentinsert": "",
"billdeleted": "",
"billposted": "", "billposted": "",
"billupdated": "", "billupdated": "",
"failedpayment": "", "failedpayment": "",
@@ -114,6 +115,7 @@
"jobassignmentremoved": "", "jobassignmentremoved": "",
"jobchecklist": "", "jobchecklist": "",
"jobconverted": "", "jobconverted": "",
"jobexported": "",
"jobfieldchanged": "", "jobfieldchanged": "",
"jobimported": "", "jobimported": "",
"jobinproductionchange": "", "jobinproductionchange": "",
@@ -126,7 +128,9 @@
"jobspartsorder": "", "jobspartsorder": "",
"jobspartsreturn": "", "jobspartsreturn": "",
"jobstatuschange": "", "jobstatuschange": "",
"jobsupplement": "" "jobsupplement": "",
"jobsuspend": "",
"jobvoid": ""
} }
}, },
"billlines": { "billlines": {

View File

@@ -20,6 +20,8 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }), i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
appointmentinsert: (start) => appointmentinsert: (start) =>
i18n.t("audit_trail.messages.appointmentinsert", { start }), i18n.t("audit_trail.messages.appointmentinsert", { start }),
billdeleted: (invoice_number) =>
i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
billposted: (invoice_number) => billposted: (invoice_number) =>
i18n.t("audit_trail.messages.billposted", { invoice_number }), i18n.t("audit_trail.messages.billposted", { invoice_number }),
billupdated: (invoice_number) => billupdated: (invoice_number) =>
@@ -33,6 +35,7 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }), i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
jobconverted: (ro_number) => jobconverted: (ro_number) =>
i18n.t("audit_trail.messages.jobconverted", { ro_number }), i18n.t("audit_trail.messages.jobconverted", { ro_number }),
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
jobfieldchange: (field, value) => jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
jobimported: () => i18n.t("audit_trail.messages.jobimported"), jobimported: () => i18n.t("audit_trail.messages.jobimported"),
@@ -51,6 +54,8 @@ const AuditTrailMapping = {
jobstatuschange: (status) => jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", { status }), i18n.t("audit_trail.messages.jobstatuschange", { status }),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }),
jobvoid: () => i18n.t("audit_trail.messages.jobvoid"),
}; };
export default AuditTrailMapping; export default AuditTrailMapping;