diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index fe1e5e8bc..de7f4c2b7 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1138,6 +1138,111 @@ messages + + billposted + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + billupdated + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobassignmentchange + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobassignmentremoved + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobchecklist + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobconverted false @@ -1159,6 +1264,27 @@ + + jobfieldchanged + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobimported false @@ -1180,6 +1306,90 @@ + + jobinproductionchange + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobmodifylbradj + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobspartsorder + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + jobspartsreturn + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobstatuschange false diff --git a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx index 64c2e5652..a63832049 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx @@ -26,6 +26,8 @@ import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-bu import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { setModalContext } from "../../redux/modals/modals.actions"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -33,6 +35,8 @@ const mapStateToProps = createStructuredSelector({ const mapDispatchToProps = (dispatch) => ({ setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })), + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), }); export default connect( @@ -40,7 +44,10 @@ export default connect( mapDispatchToProps )(BillDetailEditcontainer); -export function BillDetailEditcontainer({ setPartsOrderContext }) { +export function BillDetailEditcontainer({ + setPartsOrderContext, + insertAuditTrail, +}) { const search = queryString.parse(useLocation().search); const history = useHistory(); const { t } = useTranslation(); @@ -134,6 +141,12 @@ export function BillDetailEditcontainer({ setPartsOrderContext }) { }); await Promise.all(updates); + insertAuditTrail({ + jobid: bill.jobid, + billid: search.billid, + operation: AuditTrailMapping.billupdated(bill.invoice_number), + }); + await refetch(); form.setFieldsValue(transformData(data)); form.resetFields(); diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx index cb53d15d3..bd45d8c5e 100644 --- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx +++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx @@ -11,12 +11,14 @@ import { QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB, } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { selectBillEnterModal } from "../../redux/modals/modals.selectors"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; import BillFormContainer from "../bill-form/bill-form.container"; import { handleUpload } from "../documents-upload/documents-upload.utility"; @@ -27,6 +29,8 @@ const mapStateToProps = createStructuredSelector({ }); const mapDispatchToProps = (dispatch) => ({ toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), }); function BillEnterModalContainer({ @@ -34,6 +38,7 @@ function BillEnterModalContainer({ toggleModalVisible, bodyshop, currentUser, + insertAuditTrail, }) { const [form] = Form.useForm(); const { t } = useTranslation(); @@ -83,7 +88,7 @@ function BillEnterModalContainer({ }, refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"], }); - console.log("adjustmentsToInsert", adjustmentsToInsert); + const adjKeys = Object.keys(adjustmentsToInsert); if (adjKeys.length > 0) { //Query the adjustments, merge, and update them. @@ -116,7 +121,12 @@ function BillEnterModalContainer({ message: JSON.stringify(jobUpdate.errors), }), }); + return; } + insertAuditTrail({ + jobid: values.jobid, + operation: AuditTrailMapping.jobmodifylbradj(), + }); } if (!!r1.errors) { @@ -172,6 +182,12 @@ function BillEnterModalContainer({ }); if (billEnterModal.actions.refetch) billEnterModal.actions.refetch(); + insertAuditTrail({ + jobid: values.jobid, + billid: billId, + operation: AuditTrailMapping.billposted(remainingValues.invoice_number), + }); + if (enterAgain) { form.resetFields(); form.setFieldsValue({ billlines: [] }); diff --git a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx index 2c3af76bd..8c6db2e4c 100644 --- a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx +++ b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx @@ -16,16 +16,20 @@ import { import ConfigFormComponents from "../../../config-form-components/config-form-components.component"; import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component"; import moment from "moment-business-days"; +import { insertAuditTrail } from "../../../../redux/application/application.actions"; +import AuditTrailMapping from "../../../../utils/AuditTrailMappings"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, currentUser: selectCurrentUser, }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), }); export function JobChecklistForm({ + insertAuditTrail, formItems, bodyshop, currentUser, @@ -60,10 +64,11 @@ export function JobChecklistForm({ ...job.production_vars, ...values.production_vars, note: + values.production_vars && values.production_vars.note && values.production_vars.note !== "" - ? values.production_vars.note - : job.production_vars.note, + ? job.production_vars && values.production_vars.note + : job.production_vars && job.production_vars.note, }, }), ...(type === "intake" && { @@ -114,6 +119,17 @@ export function JobChecklistForm({ if (!!!result.errors) { notification["success"]({ message: t("checklist.successes.completed") }); history.push(`/manage/jobs/${jobId}`); + + insertAuditTrail({ + jobid: jobId, + operation: AuditTrailMapping.jobchecklist( + type, + (type === "deliver" && values.removeFromProduction && false) || + (type === "intake" && values.addToProduction), + (type === "intake" && bodyshop.md_ro_statuses.default_arrived) || + (type === "deliver" && bodyshop.md_ro_statuses.default_delivered) + ), + }); } else { notification["error"]({ message: t("checklist.errors.complete", { diff --git a/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx b/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx index 966437a13..954d04970 100644 --- a/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx +++ b/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx @@ -37,8 +37,8 @@ export function JobEmployeeAssignments({ }); const [visibility, setVisibility] = useState(false); - const onChange = (e) => { - setAssignment({ ...assignment, employeeid: e }); + const onChange = (value, option) => { + setAssignment({ ...assignment, employeeid: value, name: option.name }); }; const popContent = ( @@ -56,7 +56,11 @@ export function JobEmployeeAssignments({ } > {bodyshop.employees.map((emp) => ( - + {`${emp.first_name} ${emp.last_name}`} ))} diff --git a/client/src/components/job-employee-assignments/job-employee-assignments.container.jsx b/client/src/components/job-employee-assignments/job-employee-assignments.container.jsx index 5e60b57df..71e56c53c 100644 --- a/client/src/components/job-employee-assignments/job-employee-assignments.container.jsx +++ b/client/src/components/job-employee-assignments/job-employee-assignments.container.jsx @@ -6,14 +6,34 @@ import { logImEXEvent } from "../../firebase/firebase.utils"; import { UPDATE_JOB_ASSIGNMENTS } from "../../graphql/jobs.queries"; import JobEmployeeAssignmentsComponent from "./job-employee-assignments.component"; -export default function JobEmployeeAssignmentsContainer({ job, refetch }) { +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser +}); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobEmployeeAssignmentsContainer); + +export function JobEmployeeAssignmentsContainer({ + job, + refetch, + insertAuditTrail, +}) { const { t } = useTranslation(); const [updateJob] = useMutation(UPDATE_JOB_ASSIGNMENTS); const [loading, setLoading] = useState(false); const handleAdd = async (assignment) => { setLoading(true); - const { operation, employeeid } = assignment; + const { operation, employeeid, name } = assignment; logImEXEvent("job_assign_employee", { operation }); let empAssignment = determineFieldName(operation); @@ -23,6 +43,11 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) { }); if (refetch) refetch(); + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobassignmentchange(operation, name), + }); + if (!!result.errors) { notification["error"]({ message: t("jobs.errors.assigning", { @@ -48,6 +73,10 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) { }), }); } + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobassignmentremoved(operation), + }); setLoading(false); }; diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.addtoproduction.util.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.addtoproduction.util.jsx index 0def7574c..90c8ebd8b 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.addtoproduction.util.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.addtoproduction.util.jsx @@ -1,7 +1,10 @@ import { notification } from "antd"; import i18n from "i18next"; -import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { logImEXEvent } from "../../firebase/firebase.utils"; +import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import { store } from "../../redux/store"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; export default function AddToProduction( apolloClient, @@ -21,6 +24,13 @@ export default function AddToProduction( notification["success"]({ message: i18n.t("jobs.successes.save"), }); + + store.dispatch( + insertAuditTrail({ + jobid: jobId, + operation: AuditTrailMapping.jobinproductionchange(!remove), + }) + ); if (completionCallback) completionCallback(); }) .catch((error) => { diff --git a/client/src/components/parts-order-modal/parts-order-modal.container.jsx b/client/src/components/parts-order-modal/parts-order-modal.container.jsx index 3fbba36c3..c9e091780 100644 --- a/client/src/components/parts-order-modal/parts-order-modal.container.jsx +++ b/client/src/components/parts-order-modal/parts-order-modal.container.jsx @@ -9,6 +9,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils"; import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries"; import { INSERT_NEW_PARTS_ORDERS } from "../../graphql/parts-orders.queries"; import { QUERY_ALL_VENDORS_FOR_ORDER } from "../../graphql/vendors.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; import { setEmailOptions } from "../../redux/email/email.actions"; import { setModalContext, @@ -19,6 +20,7 @@ import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; import { GenerateDocument } from "../../utils/RenderTemplate"; import { TemplateList } from "../../utils/TemplateConstants"; import AlertComponent from "../alert/alert.component"; @@ -36,6 +38,8 @@ const mapDispatchToProps = (dispatch) => ({ toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")), setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })), + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), }); export function PartsOrderModalContainer({ @@ -45,6 +49,7 @@ export function PartsOrderModalContainer({ bodyshop, setEmailOptions, setBillEnterContext, + insertAuditTrail, }) { const { t } = useTranslation(); @@ -109,6 +114,18 @@ export function PartsOrderModalContainer({ : bodyshop.md_order_statuses.default_ordered || "Ordered*", }, }); + + insertAuditTrail({ + jobid: jobId, + operation: isReturn + ? AuditTrailMapping.jobspartsreturn( + insertResult.data.insert_parts_orders.returning[0].order_number + ) + : AuditTrailMapping.jobspartsorder( + insertResult.data.insert_parts_orders.returning[0].order_number + ), + }); + if (!!jobLinesResult.errors) { notification["error"]({ message: t("parts_orders.errors.creating"), diff --git a/client/src/graphql/parts-orders.queries.js b/client/src/graphql/parts-orders.queries.js index 378ca5a07..7cd4427c9 100644 --- a/client/src/graphql/parts-orders.queries.js +++ b/client/src/graphql/parts-orders.queries.js @@ -5,6 +5,7 @@ export const INSERT_NEW_PARTS_ORDERS = gql` insert_parts_orders(objects: $po) { returning { id + order_number } } } diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index f97fdb361..6c752f480 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -5,6 +5,7 @@ import Icon, { FileImageFilled, PrinterFilled, ToolFilled, + HistoryOutlined, } from "@ant-design/icons"; import { Button, @@ -46,6 +47,8 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors import { setModalContext } from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; +import { insertAuditTrail } from "../../redux/application/application.actions"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -54,6 +57,8 @@ const mapStateToProps = createStructuredSelector({ const mapDispatchToProps = (dispatch) => ({ setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })), + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), }); export function JobsDetailPage({ setPrintCenterContext, @@ -61,6 +66,7 @@ export function JobsDetailPage({ job, mutationUpdateJob, handleSubmit, + insertAuditTrail, refetch, }) { const { t } = useTranslation(); @@ -82,6 +88,7 @@ export function JobsDetailPage({ const handleFinish = async (values) => { setLoading(true); + const result = await mutationUpdateJob({ variables: { jobId: job.id, @@ -106,6 +113,60 @@ export function JobsDetailPage({ notification["success"]({ message: t("jobs.successes.savetitle"), }); + const changedAuditFields = form.getFieldsValue( + [ + "scheduled_in", + "actual_in", + "scheduled_completion", + "actual_completion", + "scheduled_delivery", + "actual_delivery", + "date_invoiced", + "ins_co_nm", + "ded_amt", + "ded_status", + "date_exported", + "special_coverage_policy", + "ca_gst_registrant", + "ca_bc_pvrt", + "scheduled_in", + "rate_la1", + "rate_la2", + "rate_la3", + "rate_la4", + "rate_laa", + "rate_lab", + "rate_lad", + "rate_lae", + "rate_laf", + "rate_lag", + "rate_lam", + "rate_lar", + "rate_las", + "rate_lau", + "rate_ma2s", + "rate_ma2t", + "rate_ma3s", + "rate_mabl", + "rate_macs", + "rate_mapa", + "rate_mahw", + "rate_mash", + "rate_matd", + ], + (meta) => meta && meta.touched + ); + + Object.keys(changedAuditFields).forEach((key) => { + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobfieldchange( + key, + changedAuditFields[key] + ), + }); + }); + await refetch(); form.setFieldsValue(transormJobToForm(job)); form.resetFields(); @@ -283,7 +344,7 @@ export function JobsDetailPage({ - + {t("jobs.labels.audit")} } diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 54520ddb9..ed6416c20 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -85,8 +85,18 @@ }, "audit_trail": { "messages": { + "billposted": "Bill with invoice number {{invoice_number}} posted.", + "billupdated": "Bill with invoice number {{invoice_number}} updated.", + "jobassignmentchange": "Employee {{name}} assigned to {{operation}}", + "jobassignmentremoved": "Employee assignment removed for {{operation}}", + "jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.", "jobconverted": "Job converted and assigned number {{ro_number}}.", + "jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.", "jobimported": "Job imported.", + "jobinproductionchange": "Job production status set to {{inproduction}}", + "jobmodifylbradj": "Labor adjustments modified.", + "jobspartsorder": "Parts order {{order_number}} added to job.", + "jobspartsreturn": "Parts return {{order_number}} added to job.", "jobstatuschange": "Job status changed to {{status}}.", "jobsupplement": "Job supplement imported." } diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 06fc8c5e1..2514ae112 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -85,8 +85,18 @@ }, "audit_trail": { "messages": { + "billposted": "", + "billupdated": "", + "jobassignmentchange": "", + "jobassignmentremoved": "", + "jobchecklist": "", "jobconverted": "", + "jobfieldchanged": "", "jobimported": "", + "jobinproductionchange": "", + "jobmodifylbradj": "", + "jobspartsorder": "", + "jobspartsreturn": "", "jobstatuschange": "", "jobsupplement": "" } diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 7fb2e3602..5fcd1efca 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -85,8 +85,18 @@ }, "audit_trail": { "messages": { + "billposted": "", + "billupdated": "", + "jobassignmentchange": "", + "jobassignmentremoved": "", + "jobchecklist": "", "jobconverted": "", + "jobfieldchanged": "", "jobimported": "", + "jobinproductionchange": "", + "jobmodifylbradj": "", + "jobspartsorder": "", + "jobspartsreturn": "", "jobstatuschange": "", "jobsupplement": "" } diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index ab0baee92..810c3ec86 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -7,6 +7,25 @@ const AuditTrailMapping = { jobimported: () => i18n.t("audit_trail.messages.jobimported"), jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }), + jobfieldchange: (field, value) => + i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), + jobspartsorder: (order_number) => + i18n.t("audit_trail.messages.jobspartsorder", { order_number }), + jobspartsreturn: (order_number) => + i18n.t("audit_trail.messages.jobspartsreturn", { order_number }), + jobmodifylbradj: () => i18n.t("audit_trail.messages.jobmodifylbradj", {}), + billposted: (invoice_number) => + i18n.t("audit_trail.messages.billposted", { invoice_number }), + billupdated: (invoice_number) => + i18n.t("audit_trail.messages.billupdated", { invoice_number }), + jobassignmentchange: (operation, name) => + i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }), + jobassignmentremoved: (operation) => + i18n.t("audit_trail.messages.jobassignmentremoved", { operation }), + jobinproductionchange: (inproduction) => + i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }), + jobchecklist: (type, inproduction, status) => + i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }), }; export default AuditTrailMapping; diff --git a/hasura/migrations/1627501097014_alter_table_public_audit_trail_alter_column_created/down.yaml b/hasura/migrations/1627501097014_alter_table_public_audit_trail_alter_column_created/down.yaml new file mode 100644 index 000000000..cf967e3de --- /dev/null +++ b/hasura/migrations/1627501097014_alter_table_public_audit_trail_alter_column_created/down.yaml @@ -0,0 +1,6 @@ +- args: + cascade: false + read_only: false + sql: ALTER TABLE "public"."audit_trail" ALTER COLUMN "created" TYPE timestamp + without time zone; + type: run_sql diff --git a/hasura/migrations/1627501097014_alter_table_public_audit_trail_alter_column_created/up.yaml b/hasura/migrations/1627501097014_alter_table_public_audit_trail_alter_column_created/up.yaml new file mode 100644 index 000000000..07e8e90c2 --- /dev/null +++ b/hasura/migrations/1627501097014_alter_table_public_audit_trail_alter_column_created/up.yaml @@ -0,0 +1,5 @@ +- args: + cascade: false + read_only: false + sql: ALTER TABLE "public"."audit_trail" ALTER COLUMN "created" TYPE timestamptz; + type: run_sql