diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.addevent.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.addevent.jsx
deleted file mode 100644
index 28c79e80e..000000000
--- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.addevent.jsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import { useMutation } from "@apollo/client";
-import {
- Button,
- Card,
- Form,
- Input,
- Menu,
- notification,
- Popover,
- Select,
- Space,
-} from "antd";
-import dayjs from "../../utils/day";
-import React, { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { connect } from "react-redux";
-import { createStructuredSelector } from "reselect";
-import { logImEXEvent } from "../../firebase/firebase.utils";
-import { INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
-import { selectBodyshop } from "../../redux/user/user.selectors";
-import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
-
-const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
-});
-const mapDispatchToProps = (dispatch) => ({
- //setUserLanguage: language => dispatch(setUserLanguage(language))
-});
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(JobsDetailHeaderAddEvent);
-
-export function JobsDetailHeaderAddEvent({ bodyshop, jobid, ...props }) {
- const { t } = useTranslation();
- const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
-
- const [loading, setLoading] = useState(false);
- const [form] = Form.useForm();
- const [visibility, setVisibility] = useState(false);
-
- const handleFinish = async (values) => {
- logImEXEvent("schedule_manual_event");
-
- setLoading(true);
- try {
- insertAppointment({
- variables: {
- apt: { ...values, isintake: false, jobid, bodyshopid: bodyshop.id },
- },
- refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"],
- });
- notification.open({
- type: "success",
- message: t("appointments.successes.created"),
- });
- } catch (error) {
- console.log(error);
- } finally {
- setLoading(false);
- setVisibility(false);
- }
- };
-
- const overlay = (
-
-
-
-
-
-
-
-
-
- {
- const start = form.getFieldValue("start");
- form.setFieldsValue({ end: start.add(30, "minutes") });
- }}
- />
-
- ({
- async validator(rule, value) {
- if (value) {
- const { start } = form.getFieldsValue();
- if (dayjs(start).isAfter(dayjs(value))) {
- return Promise.reject(
- t("employees.labels.endmustbeafterstart")
- );
- } else {
- return Promise.resolve();
- }
- } else {
- return Promise.resolve();
- }
- },
- }),
- ]}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-
- const handleClick = (e) => {
- setVisibility(true);
- };
-
- // TODO - Client Update - Why is this a menu item?
- return (
-
-
- {t("appointments.labels.manualevent")}
-
-
- );
-}
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 286703536..f602d3d98 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
@@ -1,575 +1,977 @@
-import { DownCircleFilled } from "@ant-design/icons";
-import { useApolloClient, useMutation } from "@apollo/client";
-import {
- Button,
- Dropdown,
- Form,
- Menu,
- Popconfirm,
- Popover,
- Select,
- notification,
-} from "antd";
-import React, { useMemo } from "react";
-import { useTranslation } from "react-i18next";
-import { connect } from "react-redux";
-import { Link, useNavigate } from "react-router-dom";
-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 {DownCircleFilled} from "@ant-design/icons";
+import {useApolloClient, useMutation} from "@apollo/client";
+import {Button, Card, Dropdown, Form, Input, notification, Popconfirm, Popover, Select, Space,} from "antd";
+import React, {useMemo, useState} from "react";
+import {useTranslation} from "react-i18next";
+import {connect} from "react-redux";
+import {Link, useNavigate} from "react-router-dom";
+import {createStructuredSelector} from "reselect";
+import {auth, logImEXEvent} from "../../firebase/firebase.utils";
+import {CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT} 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";
-import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
-import JobsDetailHeaderActionsExportcustdataComponent from "./jobs-detail-header-actions.exportcustdata.component";
+import axios from "axios";
+import {setEmailOptions} from "../../redux/email/email.actions";
+import {openChatByPhone, setMessage} from "../../redux/messaging/messaging.actions";
+import {GET_CURRENT_QUESTIONSET_ID, INSERT_CSI} from "../../graphql/csi.queries";
+import {TemplateList} from "../../utils/TemplateConstants";
+import parsePhoneNumber from "libphonenumber-js";
+import {HasFeatureAccess} from "../feature-wrapper/feature-wrapper.component";
+import {DateTimeFormatter} from "../../utils/DateFormatter";
+import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
+import dayjs from "../../utils/day";
const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
- jobRO: selectJobReadOnly,
- currentUser: selectCurrentUser,
+ bodyshop: selectBodyshop,
+ jobRO: selectJobReadOnly,
+ currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
- setScheduleContext: (context) =>
- dispatch(setModalContext({ context: context, modal: "schedule" })),
- setBillEnterContext: (context) =>
- dispatch(setModalContext({ context: context, modal: "billEnter" })),
- setPaymentContext: (context) =>
- dispatch(setModalContext({ context: context, modal: "payment" })),
- setJobCostingContext: (context) =>
- dispatch(setModalContext({ context: context, modal: "jobCosting" })),
- setTimeTicketContext: (context) =>
- dispatch(setModalContext({ context: context, modal: "timeTicket" })),
- setCardPaymentContext: (context) =>
- dispatch(setModalContext({ context: context, modal: "cardPayment" })),
- insertAuditTrail: ({ jobid, operation }) =>
- dispatch(insertAuditTrail({ jobid, operation })),
+ setScheduleContext: (context) =>
+ dispatch(setModalContext({context: context, modal: "schedule"})),
+ setBillEnterContext: (context) =>
+ dispatch(setModalContext({context: context, modal: "billEnter"})),
+ setPaymentContext: (context) =>
+ dispatch(setModalContext({context: context, modal: "payment"})),
+ setJobCostingContext: (context) =>
+ dispatch(setModalContext({context: context, modal: "jobCosting"})),
+ setTimeTicketContext: (context) =>
+ dispatch(setModalContext({context: context, modal: "timeTicket"})),
+ setCardPaymentContext: (context) =>
+ dispatch(setModalContext({context: context, modal: "cardPayment"})),
+ insertAuditTrail: ({jobid, operation}) =>
+ dispatch(insertAuditTrail({jobid, operation})),
+ setEmailOptions: (e) => dispatch(setEmailOptions(e)),
+ openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
+ setMessage: (text) => dispatch(setMessage(text)),
});
-export function JobsDetailHeaderActions({
- job,
- bodyshop,
- currentUser,
- refetch,
- setScheduleContext,
- setBillEnterContext,
- setPaymentContext,
- setJobCostingContext,
- jobRO,
- setTimeTicketContext,
- setCardPaymentContext,
- insertAuditTrail,
-}) {
- const { t } = useTranslation();
- const client = useApolloClient();
- const history = useNavigate();
- const [deleteJob] = useMutation(DELETE_JOB);
- const [updateJob] = useMutation(UPDATE_JOB);
- const [voidJob] = useMutation(VOID_JOB);
- const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
- const jobInProduction = useMemo(() => {
- return bodyshop.md_ro_statuses.production_statuses.includes(job.status);
- }, [job, bodyshop.md_ro_statuses.production_statuses]);
+export function JobsDetailHeaderActions({job, bodyshop, currentUser, refetch, setScheduleContext, setBillEnterContext, setPaymentContext, setJobCostingContext, jobRO, setTimeTicketContext, setCardPaymentContext, insertAuditTrail, setEmailOptions, openChatByPhone, setMessage }) {
+ const {t} = useTranslation();
+ const client = useApolloClient();
+ const history = useNavigate();
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+ const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
+ const [deleteJob] = useMutation(DELETE_JOB);
+ const [insertCsi] = useMutation(INSERT_CSI);
+ const [updateJob] = useMutation(UPDATE_JOB);
+ const [voidJob] = useMutation(VOID_JOB);
+ const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
+ const jobInProduction = useMemo(() => {
+ return bodyshop.md_ro_statuses.production_statuses.includes(job.status);
+ }, [job, bodyshop.md_ro_statuses.production_statuses]);
+ const [visibility, setVisibility] = useState(false);
- const jobInPreProduction = useMemo(() => {
- return bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status);
- }, [job.status, bodyshop.md_ro_statuses.pre_production_statuses]);
+ const jobInPreProduction = useMemo(() => {
+ return bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status);
+ }, [job.status, bodyshop.md_ro_statuses.pre_production_statuses]);
- const jobInPostProduction = useMemo(() => {
- return bodyshop.md_ro_statuses.post_production_statuses.includes(
- job.status
- );
- }, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
+ const jobInPostProduction = useMemo(() => {
+ return bodyshop.md_ro_statuses.post_production_statuses.includes(
+ job.status
+ );
+ }, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
- const handleAlertToggle = (e) => {
- logImEXEvent("production_toggle_alert");
- //e.stopPropagation();
- updateJob({
- variables: {
- jobId: job.id,
- job: {
- production_vars: {
- ...job.production_vars,
- alert:
- !!job.production_vars && !!job.production_vars.alert
- ? !job.production_vars.alert
- : true,
- },
- },
- },
- });
- insertAuditTrail({
- jobid: job.id,
- operation: AuditTrailMapping.alertToggle(
- !!job.production_vars && !!job.production_vars.alert
- ? !job.production_vars.alert
- : true
- ),
- });
- };
+ const handleFinish = async (values) => {
+ logImEXEvent("schedule_manual_event");
- const handleSuspend = (e) => {
- logImEXEvent("production_toggle_alert");
- //e.stopPropagation();
- updateJob({
- variables: {
- jobId: job.id,
- job: {
- suspended: !job.suspended,
- },
- },
- });
- };
-
- const statusmenu = (
-
- );
- return (
-
-
-
- );
+ if (e.key === "text") {
+ const p = parsePhoneNumber(job.ownr_ph1, "CA");
+ if (p && p.isValid()) {
+ openChatByPhone({
+ phone_num: p.formatInternational(),
+ jobid: job.id,
+ });
+ setMessage(
+ `${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
+ );
+ } else {
+ notification["error"]({
+ message: t("messaging.error.invalidphone"),
+ });
+ }
+ }
+ if (e.key === "generate") {
+ //copy it to clipboard.
+ navigator.clipboard.writeText(
+ `${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
+ );
+ }
+ } else {
+ notification["error"]({
+ message: t("csi.errors.notconfigured"),
+ });
+ }
+ }
+ else {
+ if (e.key === "email")
+ setEmailOptions({
+ jobid: job.id,
+ messageOptions: {
+ to: [job.ownr_ea],
+ replyTo: bodyshop.email,
+ },
+ template: {
+ name: TemplateList("job_special").csi_invitation_action.key,
+ variables: {
+ id: job.csiinvites[0].id,
+ },
+ },
+ });
+
+ if (e.key === "text") {
+ const p = parsePhoneNumber(job.ownr_ph1, "CA");
+ if (p && p.isValid()) {
+ openChatByPhone({
+ phone_num: p.formatInternational(),
+ jobid: job.id,
+ });
+ setMessage(
+ `${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`
+ );
+ } else {
+ notification["error"]({
+ message: t("messaging.error.invalidphone"),
+ });
+ }
+ }
+
+ if (e.key === "generate") {
+ //copy it to clipboard.
+ navigator.clipboard.writeText(
+ `${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`
+ );
+ }
+ }
+ };
+
+ const handleExportCustData = async (e) => {
+ logImEXEvent("job_export_cust_data");
+ let PartnerResponse;
+ if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
+ PartnerResponse = await axios.post(`/qbo/receivables`, {
+ jobIds: [job.id],
+ custDataOnly: true,
+ });
+ } else {
+ //Default is QBD
+
+ let QbXmlResponse;
+ try {
+ QbXmlResponse = await axios.post(
+ "/accounting/qbxml/receivables",
+ {jobIds: [job.id], custDataOnly: true},
+ {
+ headers: {
+ Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
+ },
+ }
+ );
+ console.log("handle -> XML", QbXmlResponse);
+ } catch (error) {
+ console.log("Error getting QBXML from Server.", error);
+ notification["error"]({
+ message: t("jobs.errors.exporting", {
+ error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
+ }),
+ });
+
+ return;
+ }
+
+ //let PartnerResponse;
+ try {
+ PartnerResponse = await axios.post(
+ "http://localhost:1337/qb/",
+ QbXmlResponse.data,
+ {
+ headers: {
+ Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
+ },
+ }
+ );
+ } catch (error) {
+ console.log("Error connecting to quickbooks or partner.", error);
+ notification["error"]({
+ message: t("jobs.errors.exporting-partner"),
+ });
+
+ return;
+ }
+ }
+ //Check to see if any of them failed. If they didn't execute the update.
+ const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
+ if (failedTransactions.length > 0) {
+ //Uh oh. At least one was no good.
+ failedTransactions.forEach((ft) => {
+ //insert failed export log
+ notification.open({
+ // key: "failedexports",
+ type: "error",
+ message: t("jobs.errors.exporting", {
+ error: ft.errorMessage || "",
+ }),
+ });
+ });
+
+ //Handle Failures.
+ } else {
+ //Insert success export log.
+
+ notification.open({
+ type: "success",
+ key: "jobsuccessexport",
+ message: t("jobs.successes.exported"),
+ });
+ }
+ };
+
+ const handleAlertToggle = (e) => {
+ logImEXEvent("production_toggle_alert");
+ //e.stopPropagation();
+ updateJob({
+ variables: {
+ jobId: job.id,
+ job: {
+ production_vars: {
+ ...job.production_vars,
+ alert:
+ !!job.production_vars && !!job.production_vars.alert
+ ? !job.production_vars.alert
+ : true,
+ },
+ },
+ },
+ });
+ insertAuditTrail({
+ jobid: job.id,
+ operation: AuditTrailMapping.alertToggle(
+ !!job.production_vars && !!job.production_vars.alert
+ ? !job.production_vars.alert
+ : true
+ ),
+ });
+ };
+
+ const handleSuspend = (e) => {
+ logImEXEvent("production_toggle_alert");
+ //e.stopPropagation();
+ updateJob({
+ variables: {
+ jobId: job.id,
+ job: {
+ suspended: !job.suspended,
+ },
+ },
+ });
+ };
+
+ const overlay = (
+
+
+
+
+
+
+
+
+
+ {
+ const start = form.getFieldValue("start");
+ form.setFieldsValue({ end: start.add(30, "minutes") });
+ }}
+ />
+
+ ({
+ async validator(rule, value) {
+ if (value) {
+ const { start } = form.getFieldsValue();
+ if (dayjs(start).isAfter(dayjs(value))) {
+ return Promise.reject(
+ t("employees.labels.endmustbeafterstart")
+ );
+ } else {
+ return Promise.resolve();
+ }
+ } else {
+ return Promise.resolve();
+ }
+ },
+ }),
+ ]}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ const menuItems = [
+ {
+ disabled: !jobInPreProduction || !job.converted || jobRO,
+ label: t("jobs.actions.schedule"),
+ onClick: () => {
+ logImEXEvent("job_header_schedule");
+ setScheduleContext({
+ actions: {refetch: refetch},
+ context: {
+ jobId: job.id,
+ job: job,
+ alt_transport: job.alt_transport,
+ },
+ });
+ },
+ },
+ {
+ disabled: job.status !== bodyshop.md_ro_statuses.default_scheduled,
+ label: job.status !== bodyshop.md_ro_statuses.default_scheduled ? (
+ t("menus.jobsactions.cancelallappointments")
+ ) : (
+ {
+ const jobUpdate = await cancelAllAppointments({
+ variables: {
+ jobid: 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) {
+ notification["success"]({
+ message: t("appointments.successes.canceled"),
+ });
+ insertAuditTrail({
+ jobid: job.id,
+ operation:
+ AuditTrailMapping.appointmentcancel(lost_sale_reason),
+ });
+
+ }
+ }}
+ >
+
+
+
+
+ }
+ >
+ {t("menus.jobsactions.cancelallappointments")}
+
+ )
+ },
+ {
+ disabled: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO,
+ label: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO ? (
+ t("jobs.actions.intake")
+ ) : (
+
+ {t("jobs.actions.intake")}
+
+ )
+ },
+ {
+ disabled: !jobInProduction || jobRO,
+ label: !jobInProduction ? (
+ t("jobs.actions.deliver")
+ ) : (
+
+ {t("jobs.actions.deliver")}
+
+ )
+ },
+ {
+ disabled: !job.converted,
+ label:
+ {t("jobs.actions.viewchecklist")}
+
+ },
+ {
+ key: "entertimetickets",
+ disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
+ label: t("timetickets.actions.enter"),
+ onClick: () => {
+ logImEXEvent("job_header_enter_time_ticekts");
+
+ setTimeTicketContext({
+ actions: {},
+ context: {
+ jobId: job.id,
+ created_by: currentUser.displayName
+ ? currentUser.email.concat(" | ", currentUser.displayName)
+ : currentUser.email,
+ },
+ });
+ }
+ },
+ {
+ key: 'enterpayments',
+ disabled: !job.converted,
+ label: t("menus.header.enterpayment"),
+ onClick: () => {
+ logImEXEvent("job_header_enter_payment");
+
+ setPaymentContext({
+ actions: {},
+ context: {jobid: job.id},
+ });
+ }
+ },
+ {
+ key: 'entercardpayments',
+ disabled: !job.converted,
+ label: t("menus.header.entercardpayment"),
+ onClick: () => {
+ logImEXEvent("job_header_enter_card_payment");
+
+ setCardPaymentContext({
+ actions: {},
+ context: {jobid: job.id},
+ });
+ }
+ },
+ {
+ key: 'cccontract',
+ disabled: jobRO || !job.converted,
+ label:
+ {t("menus.jobsactions.newcccontract")}
+
+ }
+ ];
+
+ menuItems.push(
+ job.inproduction ?
+ {
+ key: 'addtoproduction',
+ disabled: !job.converted,
+ label: t("jobs.actions.removefromproduction"),
+ onClick: () => AddToProduction(client, job.id, refetch, true)
+ } :
+ {
+ key: 'addtoproduction',
+ disabled: !job.converted,
+ label: t("jobs.actions.addtoproduction"),
+ onClick: () => AddToProduction(client, job.id, refetch)
+ }
+ );
+
+ menuItems.push(
+ {
+ key: 'togglesuspend',
+ onClick: handleSuspend,
+ label: job.suspended
+ ? t("production.actions.unsuspend")
+ : t("production.actions.suspend")
+ },
+ {
+ key: 'toggleAlert',
+ onClick: handleAlertToggle,
+ label: job.production_vars && job.production_vars.alert
+ ? t("production.labels.alertoff")
+ : t("production.labels.alerton")
+ },
+ {
+ key: 'dupe',
+ label: t("menus.jobsactions.duplicate"),
+ children: [
+ {
+ label: e.stopPropagation()}
+ onConfirm={() =>
+ DuplicateJob(
+ client,
+ job.id,
+ {defaultOpenStatus: bodyshop.md_ro_statuses.default_imported},
+ (newJobId) => {
+ history(`/manage/jobs/${newJobId}`);
+ notification["success"]({
+ message: t("jobs.successes.duplicated"),
+ });
+ },
+ true
+ )
+ }
+ getPopupContainer={(trigger) => trigger.parentNode}
+ >
+ {t("menus.jobsactions.duplicate")}
+
+ },
+ {
+ label: e.stopPropagation()}
+ onConfirm={() =>
+ DuplicateJob(
+ client,
+ job.id,
+ {defaultOpenStatus: bodyshop.md_ro_statuses.default_imported},
+ (newJobId) => {
+ history(`/manage/jobs/${newJobId}`);
+ notification["success"]({
+ message: t("jobs.successes.duplicated"),
+ });
+ }
+ )
+ }
+ getPopupContainer={(trigger) => trigger.parentNode}
+ >
+ {t("menus.jobsactions.duplicatenolines")}
+
+ }
+ ]
+ },
+ {
+ key: 'postbills',
+ disabled: !job.converted,
+ label: t("jobs.actions.postbills"),
+ onClick: () => {
+ logImEXEvent("job_header_enter_bills");
+
+ setBillEnterContext({
+ actions: {refetch: refetch},
+ context: {
+ job: job,
+ },
+ });
+ }
+ },
+ {
+ key: 'addtopartsqueue',
+ disabled: !job.converted || !jobInProduction || jobRO,
+ label: t("jobs.actions.addtopartsqueue"),
+ onClick: async () => {
+ const result = await updateJob({
+ variables: {
+ jobId: job.id,
+ job: {queued_for_parts: true},
+ },
+ });
+
+ if (!!!result.errors) {
+ notification["success"]({
+ message: t("jobs.successes.partsqueue"),
+ });
+ } else {
+ notification["error"]({
+ message: t("jobs.errors.saving", {
+ error: JSON.stringify(result.errors),
+ }),
+ });
+ }
+ }
+ },
+ {
+ key: 'closejob',
+ disabled: !jobInPostProduction,
+ label: !jobInPostProduction ? (
+ t("menus.jobsactions.closejob")
+ ) : (
+
+ {t("menus.jobsactions.closejob")}
+
+ )
+ },
+ {
+ key: 'admin',
+ label:
+ {t("menus.jobsactions.admin")}
+
+ }
+ );
+
+ menuItems.push(
+ {
+ key: 'exportcustdata',
+ disabled: !job.converted,
+ label: t("jobs.actions.exportcustdata"),
+ onClick: handleExportCustData
+ }
+ );
+
+ if (HasFeatureAccess({featureName: "csi", bodyshop})) {
+ const children = [
+ {
+ key: 'email',
+ disabled: !!!job.ownr_ea,
+ label: t("general.labels.email"),
+ onClick: handleCreateCsi
+ },
+ {
+ key: 'text',
+ disabled: !!!job.ownr_ph1,
+ label: t("general.labels.text"),
+ onClick: handleCreateCsi
+ },
+ {
+ key: 'generate',
+ disabled: job.csiinvites && job.csiinvites.length > 0,
+ label: t("jobs.actions.generatecsi"),
+ onClick: handleCreateCsi
+ },
+ ];
+
+ if (job.csiinvites.length) {
+ children.push(
+ {
+ type: "divider"
+ },
+ ...job.csiinvites.map((item, idx) => {
+ return item.completedon ?
+ {
+ key: idx,
+ label:
+ {item.completedon}
+
+ } :
+ {
+ key: idx,
+ onClick: () => {
+ navigator.clipboard.writeText(
+ `${window.location.protocol}//${window.location.host}/csi/${item.id}`
+ );
+ },
+ label: t("general.actions.copylink")
+ }
+ }),
+ )
+ }
+ menuItems.push(
+ {
+ key: 'sendcsi',
+ label: t("jobs.actions.sendcsi"),
+ disabled: !job.converted,
+ children
+ }
+ );
+ }
+
+ menuItems.push({
+ key: 'jobcosting',
+ disabled: !job.converted,
+ label: t("jobs.labels.jobcosting"),
+ onClick: () => {
+ logImEXEvent("job_header_job_costing");
+
+ setJobCostingContext({
+ actions: {refetch: refetch},
+ context: {
+ jobId: job.id,
+ },
+ });
+ }
+ }
+ );
+
+ if (job && !job.converted) {
+ menuItems.push(
+ {
+ label: e.stopPropagation()}
+ onConfirm={async () => {
+ //delete the job.
+ const result = await deleteJob({variables: {id: job.id}});
+
+ if (!!!result.errors) {
+ notification["success"]({
+ message: t("jobs.successes.delete"),
+ });
+ //go back to jobs list.
+ history(`/manage/`);
+ } else {
+ notification["error"]({
+ message: t("jobs.errors.deleted", {
+ error: JSON.stringify(result.errors),
+ }),
+ });
+ }
+ }}
+ getPopupContainer={(trigger) => trigger.parentNode}
+ >
+ {t("menus.jobsactions.deletejob")}
+
+ }
+ );
+ }
+
+ menuItems.push(
+ {
+ onClick: (e) => {
+ setVisibility(true);
+ },
+ label: t("appointments.labels.manualevent")
+ }
+ )
+
+ if (!jobRO && job.converted) {
+ menuItems.push({
+ label:
+ e.stopPropagation()}
+ onConfirm={async () => {
+ //delete the job.
+ const result = await voidJob({
+ variables: {
+ jobId: job.id,
+ job: {
+ status: bodyshop.md_ro_statuses.default_void,
+ voided: true,
+ scheduled_in: null,
+ scheduled_completion: null,
+ inproduction: false,
+ date_void: new Date(),
+ },
+ note: [
+ {
+ jobid: job.id,
+ created_by: currentUser.email,
+ audit: true,
+ text: t("jobs.labels.voidnote"),
+ },
+ ],
+ },
+ });
+
+ if (!!!result.errors) {
+ notification["success"]({
+ message: t("jobs.successes.voided"),
+ });
+ //go back to jobs list.
+ history(`/manage/`);
+ } else {
+ notification["error"]({
+ message: t("jobs.errors.voiding", {
+ error: JSON.stringify(result.errors),
+ }),
+ });
+ }
+ }}
+ getPopupContainer={(trigger) => trigger.parentNode}
+ >
+ {t("menus.jobsactions.void")}
+
+
+ });
+ }
+
+ const menu = {
+ items: menuItems,
+ key: 'popovermenu'
+ }
+
+ return (
+ <>
+
+
+
+
+ >
+ );
}
+
export default connect(
- mapStateToProps,
- mapDispatchToProps
+ mapStateToProps,
+ mapDispatchToProps
)(JobsDetailHeaderActions);
diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx
deleted file mode 100644
index bb35f3475..000000000
--- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx
+++ /dev/null
@@ -1,240 +0,0 @@
-import { useApolloClient, useMutation } from "@apollo/client";
-import { Menu, notification } from "antd";
-import parsePhoneNumber from "libphonenumber-js";
-import React from "react";
-import { useTranslation } from "react-i18next";
-import { connect } from "react-redux";
-import { Link } from "react-router-dom";
-import { createStructuredSelector } from "reselect";
-import { logImEXEvent } from "../../firebase/firebase.utils";
-import {
- GET_CURRENT_QUESTIONSET_ID,
- INSERT_CSI,
-} from "../../graphql/csi.queries";
-import { setEmailOptions } from "../../redux/email/email.actions";
-import {
- openChatByPhone,
- setMessage,
-} from "../../redux/messaging/messaging.actions";
-import { selectBodyshop } from "../../redux/user/user.selectors";
-import { DateTimeFormatter } from "../../utils/DateFormatter";
-import { TemplateList } from "../../utils/TemplateConstants";
-import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
-
-const mapStateToProps = createStructuredSelector({
- //currentUser: selectCurrentUser'
- bodyshop: selectBodyshop,
-});
-const mapDispatchToProps = (dispatch) => ({
- setEmailOptions: (e) => dispatch(setEmailOptions(e)),
- openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
- setMessage: (text) => dispatch(setMessage(text)),
-});
-
-export function JobsDetailHeaderCsi({
- setEmailOptions,
- bodyshop,
- job,
- openChatByPhone,
- setMessage,
- ...props
-}) {
- const { t } = useTranslation();
- const [insertCsi] = useMutation(INSERT_CSI);
- const client = useApolloClient();
-
- const handleCreateCsi = async (e) => {
- logImEXEvent("job_create_csi");
-
- //Is tehre already a CSI?
- if (!job.csiinvites || job.csiinvites.length === 0) {
- const questionSetResult = await client.query({
- query: GET_CURRENT_QUESTIONSET_ID,
- });
-
- if (questionSetResult.data.csiquestions.length > 0) {
- const result = await insertCsi({
- variables: {
- csiInput: {
- jobid: job.id,
- bodyshopid: bodyshop.id,
- questionset: questionSetResult.data.csiquestions[0].id,
- relateddata: {
- job: {
- id: job.id,
- ownr_fn: job.ownr_fn,
- ro_number: job.ro_number,
- v_model_yr: job.v_model_yr,
- v_make_desc: job.v_make_desc,
- v_model_desc: job.v_model_desc,
- },
- bodyshop: {
- city: bodyshop.city,
- email: bodyshop.email,
- state: bodyshop.state,
- country: bodyshop.country,
- address1: bodyshop.address1,
- address2: bodyshop.address2,
- shopname: bodyshop.shopname,
- zip_post: bodyshop.zip_post,
- logo_img_path: bodyshop.logo_img_path,
- },
- },
- },
- },
- refetchQueries: ["GET_JOB_BY_PK"],
- awaitRefetchQueries: true,
- });
-
- if (!!!result.errors) {
- notification["success"]({ message: t("csi.successes.created") });
- } else {
- notification["error"]({
- message: t("csi.errors.creating", {
- message: JSON.stringify(result.errors),
- }),
- });
- return;
- }
- if (e.key === "email")
- setEmailOptions({
- jobid: job.id,
- messageOptions: {
- to: [job.ownr_ea],
- replyTo: bodyshop.email,
- },
- template: {
- name: TemplateList("job_special").csi_invitation_action.key,
- variables: {
- id: result.data.insert_csi.returning[0].id,
- },
- },
- });
-
- if (e.key === "text") {
- const p = parsePhoneNumber(job.ownr_ph1, "CA");
- if (p && p.isValid()) {
- openChatByPhone({
- phone_num: p.formatInternational(),
- jobid: job.id,
- });
- setMessage(
- `${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
- );
- } else {
- notification["error"]({
- message: t("messaging.error.invalidphone"),
- });
- }
- }
- if (e.key === "generate") {
- //copy it to clipboard.
- navigator.clipboard.writeText(
- `${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
- );
- }
- } else {
- notification["error"]({
- message: t("csi.errors.notconfigured"),
- });
- }
- } else {
- if (e.key === "email")
- setEmailOptions({
- jobid: job.id,
- messageOptions: {
- to: [job.ownr_ea],
- replyTo: bodyshop.email,
- },
- template: {
- name: TemplateList("job_special").csi_invitation_action.key,
- variables: {
- id: job.csiinvites[0].id,
- },
- },
- });
-
- if (e.key === "text") {
- const p = parsePhoneNumber(job.ownr_ph1, "CA");
- if (p && p.isValid()) {
- openChatByPhone({
- phone_num: p.formatInternational(),
- jobid: job.id,
- });
- setMessage(
- `${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`
- );
- } else {
- notification["error"]({
- message: t("messaging.error.invalidphone"),
- });
- }
- }
-
- if (e.key === "generate") {
- //copy it to clipboard.
- navigator.clipboard.writeText(
- `${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`
- );
- }
- }
- };
-
- if (!HasFeatureAccess({ featureName: "csi", bodyshop })) return <>>;
-
- return (
-
-
- {t("general.labels.email")}
-
-
- {t("general.labels.text")}
-
- 0}
- >
- {t("jobs.actions.generatecsi")}
-
-
- {job.csiinvites.map((item, idx) => {
- return item.completedon ? (
-
-
- {item.completedon}
-
-
- ) : (
- {
- navigator.clipboard.writeText(
- `${window.location.protocol}//${window.location.host}/csi/${item.id}`
- );
- }}
- >
- {t("general.actions.copylink")}
-
- );
- })}
-
- );
-}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(JobsDetailHeaderCsi);
diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.exportcustdata.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.exportcustdata.component.jsx
deleted file mode 100644
index 671d49929..000000000
--- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.exportcustdata.component.jsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import { Menu, notification } from "antd";
-import axios from "axios";
-import React from "react";
-import { useTranslation } from "react-i18next";
-import { connect } from "react-redux";
-import { createStructuredSelector } from "reselect";
-import { auth, logImEXEvent } from "../../firebase/firebase.utils";
-import { selectBodyshop } from "../../redux/user/user.selectors";
-
-const mapStateToProps = createStructuredSelector({
- bodyshop: selectBodyshop,
-});
-const mapDispatchToProps = (dispatch) => ({});
-
-export function JobsDetailHeaderActionexportCustomerData({
- bodyshop,
- job,
- ...props
-}) {
- const { t } = useTranslation();
-
- const handleExportCustData = async (e) => {
- logImEXEvent("job_export_cust_data");
- let PartnerResponse;
- if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
- PartnerResponse = await axios.post(`/qbo/receivables`, {
- jobIds: [job.id],
- custDataOnly: true,
- });
- } else {
- //Default is QBD
-
- let QbXmlResponse;
- try {
- QbXmlResponse = await axios.post(
- "/accounting/qbxml/receivables",
- { jobIds: [job.id], custDataOnly: true },
- {
- headers: {
- Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
- },
- }
- );
- console.log("handle -> XML", QbXmlResponse);
- } catch (error) {
- console.log("Error getting QBXML from Server.", error);
- notification["error"]({
- message: t("jobs.errors.exporting", {
- error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
- }),
- });
-
- return;
- }
-
- //let PartnerResponse;
- try {
- PartnerResponse = await axios.post(
- "http://localhost:1337/qb/",
- QbXmlResponse.data,
- {
- headers: {
- Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
- },
- }
- );
- } catch (error) {
- console.log("Error connecting to quickbooks or partner.", error);
- notification["error"]({
- message: t("jobs.errors.exporting-partner"),
- });
-
- return;
- }
- }
- //Check to see if any of them failed. If they didn't don't execute the update.
- const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
- if (failedTransactions.length > 0) {
- //Uh oh. At least one was no good.
- failedTransactions.forEach((ft) => {
- //insert failed export log
- notification.open({
- // key: "failedexports",
- type: "error",
- message: t("jobs.errors.exporting", {
- error: ft.errorMessage || "",
- }),
- });
- });
-
- //Handle Failures.
- } else {
- //Insert success export log.
-
- notification.open({
- type: "success",
- key: "jobsuccessexport",
- message: t("jobs.successes.exported"),
- });
- }
- };
-
- return (
-
- {t("jobs.actions.exportcustdata")}
-
- );
-}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(JobsDetailHeaderActionexportCustomerData);