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;