diff --git a/client/src/components/job-at-change/schedule-event.container.jsx b/client/src/components/job-at-change/schedule-event.container.jsx
index 89065f0f7..60abf9c61 100644
--- a/client/src/components/job-at-change/schedule-event.container.jsx
+++ b/client/src/components/job-at-change/schedule-event.container.jsx
@@ -2,12 +2,16 @@ import { useMutation } from "@apollo/client";
import { notification } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
+import { useDispatch } from "react-redux";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { CANCEL_APPOINTMENT_BY_ID } from "../../graphql/appointments.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
+import { insertAuditTrail } from "../../redux/application/application.actions";
+import AuditTrailMapping from "../../utils/AuditTrailMappings";
import ScheduleEventComponent from "./schedule-event.component";
export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
+ const dispatch = useDispatch();
const { t } = useTranslation();
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
const [updateJob] = useMutation(UPDATE_JOB);
@@ -34,16 +38,24 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
const jobUpdate = await updateJob({
variables: {
jobId: event.job.id,
-
job: {
date_scheduled: null,
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
+ date_lost_sale: new Date(),
status: bodyshop.md_ro_statuses.default_imported,
},
},
});
+ if (!jobUpdate.errors) {
+ dispatch(
+ insertAuditTrail({
+ jobid: event.job.id,
+ operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
+ })
+ );
+ }
if (!!jobUpdate.errors) {
notification["error"]({
message: t("jobs.errors.updating", {
diff --git a/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx b/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx
index bfddbfefc..5ac31da74 100644
--- a/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx
+++ b/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx
@@ -13,6 +13,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
+import { DateTimeFormat } from "./../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -53,7 +54,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
operation: AuditTrailMapping.admin_jobfieldchange(
key,
changedAuditFields[key] instanceof moment
- ? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a")
+ ? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key]
),
});
@@ -179,6 +180,12 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
+
+
+
diff --git a/client/src/components/jobs-detail-dates/jobs-detail-dates.component.jsx b/client/src/components/jobs-detail-dates/jobs-detail-dates.component.jsx
index 05cd1b289..dcd6fd941 100644
--- a/client/src/components/jobs-detail-dates/jobs-detail-dates.component.jsx
+++ b/client/src/components/jobs-detail-dates/jobs-detail-dates.component.jsx
@@ -145,6 +145,13 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
+
+
+
+
);
diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx
index 8531ddfe0..ae3ba9a7b 100644
--- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx
+++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx
@@ -18,12 +18,14 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { CANCEL_APPOINTMENTS_BY_JOB_ID } from "../../graphql/appointments.queries";
import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
+import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
+import AuditTrailMapping from "../../utils/AuditTrailMappings";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import JobsDetailHeaderActionsAddevent from "./jobs-detail-header-actions.addevent";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
@@ -50,6 +52,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
+ insertAuditTrail: ({ jobid, operation }) =>
+ dispatch(insertAuditTrail({ jobid, operation })),
});
export function JobsDetailHeaderActions({
@@ -64,6 +68,7 @@ export function JobsDetailHeaderActions({
jobRO,
setTimeTicketContext,
setCardPaymentContext,
+ insertAuditTrail,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -158,6 +163,7 @@ export function JobsDetailHeaderActions({
scheduled_in: null,
scheduled_completion: null,
lost_sale_reason,
+ date_lost_sale: new Date(),
status: bodyshop.md_ro_statuses.default_imported,
},
},
@@ -166,6 +172,11 @@ export function JobsDetailHeaderActions({
notification["success"]({
message: t("appointments.successes.canceled"),
});
+ insertAuditTrail({
+ jobid: job.id,
+ operation:
+ AuditTrailMapping.appointmentcancel(lost_sale_reason),
+ });
return;
}
}}
diff --git a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx
index c8b85be38..19c360a21 100644
--- a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx
+++ b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx
@@ -13,6 +13,7 @@ import {
QUERY_APPOINTMENTS_BY_JOBID,
} from "../../graphql/appointments.queries";
import { QUERY_LBR_HRS_BY_PK, UPDATE_JOBS } from "../../graphql/jobs.queries";
+import { insertAuditTrail } from "../../redux/application/application.actions";
import { setEmailOptions } from "../../redux/email/email.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectSchedule } from "../../redux/modals/modals.selectors";
@@ -20,6 +21,8 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
+import AuditTrailMapping from "../../utils/AuditTrailMappings";
+import { DateTimeFormat } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import ScheduleJobModalComponent from "./schedule-job-modal.component";
@@ -31,6 +34,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
+ insertAuditTrail: ({ jobid, operation }) =>
+ dispatch(insertAuditTrail({ jobid, operation })),
});
export function ScheduleJobModalContainer({
@@ -39,6 +44,7 @@ export function ScheduleJobModalContainer({
toggleModalVisible,
setEmailOptions,
currentUser,
+ insertAuditTrail,
}) {
const { visible, context, actions } = scheduleModal;
const { jobId, job, previousEvent } = context;
@@ -134,6 +140,15 @@ export function ScheduleJobModalContainer({
},
});
+ if (!appt.errors) {
+ insertAuditTrail({
+ jobid: job.id,
+ operation: AuditTrailMapping.appointmentinsert(
+ DateTimeFormat(values.start)
+ ),
+ });
+ }
+
if (!!appt.errors) {
notification["error"]({
message: t("appointments.errors.saving", {
@@ -155,6 +170,7 @@ export function ScheduleJobModalContainer({
scheduled_in: values.start,
scheduled_completion: values.scheduled_completion,
lost_sale_reason: null,
+ date_lost_sale: null,
},
},
});
diff --git a/client/src/graphql/appointments.queries.js b/client/src/graphql/appointments.queries.js
index fa2a3d7a0..9bef78daa 100644
--- a/client/src/graphql/appointments.queries.js
+++ b/client/src/graphql/appointments.queries.js
@@ -271,6 +271,7 @@ export const CANCEL_APPOINTMENTS_BY_JOB_ID = gql`
scheduled_completion
status
lost_sale_reason
+ date_lost_sale
}
}
`;
diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js
index 21e82350b..b14e0e03b 100644
--- a/client/src/graphql/jobs.queries.js
+++ b/client/src/graphql/jobs.queries.js
@@ -675,6 +675,7 @@ export const GET_JOB_BY_PK = gql`
date_scheduled
date_invoiced
date_last_contacted
+ date_lost_sale
date_next_contact
date_towin
date_rentalresp
@@ -1077,6 +1078,7 @@ export const UPDATE_JOB = gql`
actual_in
date_repairstarted
date_void
+ date_lost_sale
}
}
}
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 c98e8b1a9..001a18166 100644
--- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx
+++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx
@@ -3,19 +3,19 @@ import Icon, {
CalendarFilled,
DollarCircleOutlined,
FileImageFilled,
- PrinterFilled,
- ToolFilled,
HistoryOutlined,
+ PrinterFilled,
SyncOutlined,
+ ToolFilled,
} from "@ant-design/icons";
import {
Button,
Divider,
Form,
- notification,
PageHeader,
Space,
Tabs,
+ notification,
} from "antd";
import Axios from "axios";
import moment from "moment";
@@ -27,6 +27,7 @@ import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
+import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
@@ -42,17 +43,17 @@ import JobsDetailPliContainer from "../../components/jobs-detail-pli/jobs-detail
import JobsDetailRates from "../../components/jobs-detail-rates/jobs-detail-rates.component";
import JobsDetailTotals from "../../components/jobs-detail-totals/jobs-detail-totals.component";
import JobsDocumentsGalleryContainer from "../../components/jobs-documents-gallery/jobs-documents-gallery.container";
+import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
+import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
+import { insertAuditTrail } from "../../redux/application/application.actions";
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";
-import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import UndefinedToNull from "../../utils/undefinedtonull";
-import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
+import { DateTimeFormat } from "./../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -172,7 +173,7 @@ export function JobsDetailPage({
operation: AuditTrailMapping.jobfieldchange(
key,
changedAuditFields[key] instanceof moment
- ? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a")
+ ? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key]
),
});
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index feb826e09..8f380d29f 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -103,6 +103,8 @@
"admin_jobmarkforreexport": "ADMIN: Job marked for re-export.",
"admin_jobuninvoice": "ADMIN: Job has been uninvoiced.",
"admin_jobunvoid": "ADMIN: Job has been unvoided.",
+ "appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.",
+ "appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
"billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
"failedpayment": "Failed payment",
@@ -1447,6 +1449,7 @@
"date_exported": "Exported",
"date_invoiced": "Invoiced",
"date_last_contacted": "Last Contacted Date",
+ "date_lost_sale": "Lost Sale",
"date_next_contact": "Next Contact Date",
"date_open": "Open",
"date_rentalresp": "Shop Rental Responsibility Start",
@@ -2601,6 +2604,7 @@
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
"jobs_scheduled_completion": "Jobs Scheduled Completion",
"lag_time": "Lag Time",
+ "lost_sales": "Lost Sales",
"open_orders": "Open Orders by Date",
"open_orders_csr": "Open Orders by CSR",
"open_orders_estimator": "Open Orders by Estimator",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 96a6989a0..5a832efcc 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -103,6 +103,8 @@
"admin_jobmarkforreexport": "",
"admin_jobuninvoice": "",
"admin_jobunvoid": "",
+ "appointmentcancel": "",
+ "appointmentinsert": "",
"billposted": "",
"billupdated": "",
"failedpayment": "",
@@ -1447,6 +1449,7 @@
"date_exported": "Exportado",
"date_invoiced": "Facturado",
"date_last_contacted": "",
+ "date_lost_sale": "",
"date_next_contact": "",
"date_open": "Abierto",
"date_rentalresp": "",
@@ -2601,6 +2604,7 @@
"jobs_reconcile": "",
"jobs_scheduled_completion": "",
"lag_time": "",
+ "lost_sales": "",
"open_orders": "",
"open_orders_csr": "",
"open_orders_estimator": "",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 0585c0f34..3330595e8 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -103,6 +103,8 @@
"admin_jobmarkforreexport": "",
"admin_jobuninvoice": "",
"admin_jobunvoid": "",
+ "appointmentcancel": "",
+ "appointmentinsert": "",
"billposted": "",
"billupdated": "",
"failedpayment": "",
@@ -1447,6 +1449,7 @@
"date_exported": "Exportés",
"date_invoiced": "Facturé",
"date_last_contacted": "",
+ "date_lost_sale": "",
"date_next_contact": "",
"date_open": "Ouvrir",
"date_rentalresp": "",
@@ -2601,6 +2604,7 @@
"jobs_reconcile": "",
"jobs_scheduled_completion": "",
"lag_time": "",
+ "lost_sales": "",
"open_orders": "",
"open_orders_csr": "",
"open_orders_estimator": "",
diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js
index 47fdc4535..afa0de1e5 100644
--- a/client/src/utils/AuditTrailMappings.js
+++ b/client/src/utils/AuditTrailMappings.js
@@ -1,6 +1,10 @@
import i18n from "i18next";
const AuditTrailMapping = {
+ appointmentcancel: (lost_sale_reason) =>
+ i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
+ appointmentinsert: (start) =>
+ i18n.t("audit_trail.messages.appointmentinsert", { start }),
jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", { status }),
admin_jobstatuschange: (status) =>
diff --git a/client/src/utils/DateFormatter.jsx b/client/src/utils/DateFormatter.jsx
index 134095c78..d034266e3 100644
--- a/client/src/utils/DateFormatter.jsx
+++ b/client/src/utils/DateFormatter.jsx
@@ -31,3 +31,7 @@ export function TimeAgoFormatter(props) {
) : null;
}
+
+export function DateTimeFormat(value) {
+ return moment(value).format("MM/DD/YYYY hh:mm A");
+}
diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js
index 79d9aa353..c5e10d712 100644
--- a/client/src/utils/TemplateConstants.js
+++ b/client/src/utils/TemplateConstants.js
@@ -2014,6 +2014,18 @@ export const TemplateList = (type, context) => {
},
group: "jobs",
},
+ lost_sales: {
+ title: i18n.t("reportcenter.templates.lost_sales"),
+ subject: i18n.t("reportcenter.templates.lost_sales"),
+ key: "lost_sales",
+ //idtype: "vendor",
+ disabled: false,
+ rangeFilter: {
+ object: i18n.t("reportcenter.labels.objects.jobs"),
+ field: i18n.t("jobs.fields.date_lost_sale"),
+ },
+ group: "customers",
+ },
}
: {}),
...(!type || type === "courtesycarcontract"
diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml
index 2043e67fc..d94a97af0 100644
--- a/hasura/metadata/tables.yaml
+++ b/hasura/metadata/tables.yaml
@@ -3587,6 +3587,7 @@
- date_exported
- date_invoiced
- date_last_contacted
+ - date_lost_sale
- date_next_contact
- date_open
- date_rentalresp
@@ -3867,6 +3868,7 @@
- date_exported
- date_invoiced
- date_last_contacted
+ - date_lost_sale
- date_next_contact
- date_open
- date_rentalresp
diff --git a/hasura/migrations/1700680020194_alter_table_public_jobs_add_column_date_lost_sale/down.sql b/hasura/migrations/1700680020194_alter_table_public_jobs_add_column_date_lost_sale/down.sql
new file mode 100644
index 000000000..1313da0c3
--- /dev/null
+++ b/hasura/migrations/1700680020194_alter_table_public_jobs_add_column_date_lost_sale/down.sql
@@ -0,0 +1,4 @@
+-- Could not auto-generate a down migration.
+-- Please write an appropriate down migration for the SQL below:
+-- alter table "public"."jobs" add column "date_lost_sale" timestamp with time zone
+-- null;
diff --git a/hasura/migrations/1700680020194_alter_table_public_jobs_add_column_date_lost_sale/up.sql b/hasura/migrations/1700680020194_alter_table_public_jobs_add_column_date_lost_sale/up.sql
new file mode 100644
index 000000000..933f0acba
--- /dev/null
+++ b/hasura/migrations/1700680020194_alter_table_public_jobs_add_column_date_lost_sale/up.sql
@@ -0,0 +1,2 @@
+alter table "public"."jobs" add column "date_lost_sale" timestamp with time zone
+ null;
diff --git a/hasura/migrations/1700682617632_alter_table_public_jobs_alter_column_date_lost_sale/down.sql b/hasura/migrations/1700682617632_alter_table_public_jobs_alter_column_date_lost_sale/down.sql
new file mode 100644
index 000000000..32c37a9fe
--- /dev/null
+++ b/hasura/migrations/1700682617632_alter_table_public_jobs_alter_column_date_lost_sale/down.sql
@@ -0,0 +1 @@
+ALTER TABLE "public"."jobs" ALTER COLUMN "date_lost_sale" TYPE timestamp with time zone;
diff --git a/hasura/migrations/1700682617632_alter_table_public_jobs_alter_column_date_lost_sale/up.sql b/hasura/migrations/1700682617632_alter_table_public_jobs_alter_column_date_lost_sale/up.sql
new file mode 100644
index 000000000..32c37a9fe
--- /dev/null
+++ b/hasura/migrations/1700682617632_alter_table_public_jobs_alter_column_date_lost_sale/up.sql
@@ -0,0 +1 @@
+ALTER TABLE "public"."jobs" ALTER COLUMN "date_lost_sale" TYPE timestamp with time zone;