1171 lines
34 KiB
JavaScript
1171 lines
34 KiB
JavaScript
import { DownCircleFilled } from "@ant-design/icons";
|
|
import { useApolloClient, useMutation } from "@apollo/client";
|
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
|
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
|
|
import axios from "axios";
|
|
import parsePhoneNumber from "libphonenumber-js";
|
|
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 { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.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 { setEmailOptions } from "../../redux/email/email.actions";
|
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
|
import { TemplateList } from "../../utils/TemplateConstants";
|
|
import dayjs from "../../utils/day";
|
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
|
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
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, type }) =>
|
|
dispatch(
|
|
insertAuditTrail({
|
|
jobid,
|
|
operation,
|
|
type
|
|
})
|
|
),
|
|
setTimeTicketTaskContext: (context) =>
|
|
dispatch(
|
|
setModalContext({
|
|
context: context,
|
|
modal: "timeTicketTask"
|
|
})
|
|
),
|
|
setTaskUpsertContext: (context) =>
|
|
dispatch(
|
|
setModalContext({
|
|
context: context,
|
|
modal: "taskUpsert"
|
|
})
|
|
),
|
|
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,
|
|
setEmailOptions,
|
|
openChatByPhone,
|
|
setMessage,
|
|
setTimeTicketTaskContext,
|
|
setTaskUpsertContext
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const client = useApolloClient();
|
|
const history = useNavigate();
|
|
const [form] = Form.useForm();
|
|
const [loading, setLoading] = useState(false);
|
|
const [isCancelScheduleModalVisible, setIsCancelScheduleModalVisible] = 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 {
|
|
treatments: { ImEXPay }
|
|
} = useSplitTreatments({
|
|
attributes: {},
|
|
names: ["ImEXPay"],
|
|
splitKey: bodyshop && bodyshop.imexshopid
|
|
});
|
|
|
|
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 jobInPostProduction = useMemo(() => {
|
|
return bodyshop.md_ro_statuses.post_production_statuses.includes(job.status);
|
|
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
|
|
|
// Function to show modal
|
|
const showCancelScheduleModal = () => {
|
|
setIsCancelScheduleModalVisible(true);
|
|
};
|
|
|
|
// Function to handle Cancel
|
|
const handleCancelScheduleModalCancel = () => {
|
|
setIsCancelScheduleModalVisible(false);
|
|
};
|
|
|
|
const handleDuplicate = () =>
|
|
DuplicateJob(
|
|
client,
|
|
job.id,
|
|
{ defaultOpenStatus: bodyshop.md_ro_statuses.default_imported },
|
|
(newJobId) => {
|
|
history(`/manage/jobs/${newJobId}`);
|
|
notification["success"]({
|
|
message: t("jobs.successes.duplicated")
|
|
});
|
|
},
|
|
true
|
|
);
|
|
|
|
const handleDuplicateConfirm = () =>
|
|
DuplicateJob(client, job.id, { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, (newJobId) => {
|
|
history(`/manage/jobs/${newJobId}`);
|
|
notification["success"]({
|
|
message: t("jobs.successes.duplicated")
|
|
});
|
|
});
|
|
|
|
const handleFinish = async (values) => {
|
|
logImEXEvent("schedule_manual_event");
|
|
|
|
setLoading(true);
|
|
try {
|
|
insertAppointment({
|
|
variables: {
|
|
apt: { ...values, isintake: false, jobid: job.id, bodyshopid: bodyshop.id }
|
|
},
|
|
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
|
});
|
|
notification.open({
|
|
type: "success",
|
|
message: t("appointments.successes.created")
|
|
});
|
|
} catch (error) {
|
|
} finally {
|
|
setLoading(false);
|
|
setVisibility(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteJob = 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)
|
|
})
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleCreateCsi = async (e) => {
|
|
logImEXEvent("job_create_csi");
|
|
|
|
//Is there 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)
|
|
})
|
|
});
|
|
insertAuditTrail({
|
|
jobid: job.id,
|
|
operation: AuditTrailMapping.jobvoid(),
|
|
type: "jobvoid"
|
|
});
|
|
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}`
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleVoidJob = 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")
|
|
});
|
|
insertAuditTrail({
|
|
jobid: job.id,
|
|
operation: AuditTrailMapping.jobvoid(),
|
|
type: "jobvoid"
|
|
});
|
|
//go back to jobs list.
|
|
history(`/manage/`);
|
|
} else {
|
|
notification["error"]({
|
|
message: t("jobs.errors.voiding", {
|
|
error: JSON.stringify(result.errors)
|
|
})
|
|
});
|
|
}
|
|
};
|
|
|
|
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
|
|
),
|
|
type: "alertToggle"
|
|
});
|
|
};
|
|
|
|
const handleSuspend = (e) => {
|
|
logImEXEvent("production_toggle_alert");
|
|
//e.stopPropagation();
|
|
updateJob({
|
|
variables: {
|
|
jobId: job.id,
|
|
job: {
|
|
suspended: !job.suspended
|
|
}
|
|
}
|
|
});
|
|
insertAuditTrail({
|
|
jobid: job.id,
|
|
operation: AuditTrailMapping.jobsuspend(!!job.suspended ? !job.suspended : true),
|
|
type: "jobsuspend"
|
|
});
|
|
};
|
|
|
|
// Function to handle OK
|
|
const handleCancelScheduleOK = async () => {
|
|
await form.submit(); // Assuming 'form' is the Form instance from useForm()
|
|
setIsCancelScheduleModalVisible(false);
|
|
};
|
|
|
|
const handleLostSaleFinish = async ({ lost_sale_reason }) => {
|
|
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),
|
|
type: "appointmentcancel"
|
|
});
|
|
}
|
|
};
|
|
|
|
const popOverContent = (
|
|
<Card>
|
|
<div>
|
|
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
|
<Form.Item
|
|
label={t("appointments.fields.title")}
|
|
name="title"
|
|
rules={[
|
|
{
|
|
required: true
|
|
//message: t("general.validation.required"),
|
|
}
|
|
]}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item label={t("appointments.fields.note")} name="note">
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t("appointments.fields.start")}
|
|
name="start"
|
|
rules={[
|
|
{
|
|
required: true
|
|
//message: t("general.validation.required"),
|
|
}
|
|
]}
|
|
>
|
|
<FormDateTimePickerComponent
|
|
onBlur={() => {
|
|
const start = form.getFieldValue("start");
|
|
form.setFieldsValue({ end: start.add(30, "minutes") });
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t("appointments.fields.end")}
|
|
name="end"
|
|
rules={[
|
|
{
|
|
required: true
|
|
//message: t("general.validation.required"),
|
|
},
|
|
({ getFieldValue }) => ({
|
|
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();
|
|
}
|
|
}
|
|
})
|
|
]}
|
|
>
|
|
<FormDateTimePickerComponent />
|
|
</Form.Item>
|
|
<Form.Item label={t("appointments.fields.color")} name="color">
|
|
<Select>
|
|
{bodyshop.appt_colors.map((col, idx) => (
|
|
<Select.Option key={idx} value={col.color.hex}>
|
|
{col.label}
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
<Space wrap>
|
|
<Button type="primary" htmlType="submit" loading={loading}>
|
|
{t("general.actions.save")}
|
|
</Button>
|
|
<Button onClick={() => setVisibility(false)}>{t("general.actions.cancel")}</Button>
|
|
</Space>
|
|
</Form>
|
|
</div>
|
|
</Card>
|
|
);
|
|
|
|
const menuItems = [
|
|
{
|
|
key: "schedule",
|
|
id: "job-actions-schedule",
|
|
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
|
|
}
|
|
});
|
|
}
|
|
},
|
|
{
|
|
key: "cancelallappointments",
|
|
id: "job-actions-cancelallappointments",
|
|
onClick: () => {
|
|
if (job.status !== bodyshop.md_ro_statuses.default_scheduled) {
|
|
return;
|
|
}
|
|
showCancelScheduleModal();
|
|
},
|
|
disabled: job.status !== bodyshop.md_ro_statuses.default_scheduled,
|
|
label: t("menus.jobsactions.cancelallappointments")
|
|
},
|
|
...InstanceRenderManager({
|
|
imex: [
|
|
{
|
|
key: "intake",
|
|
id: "job-actions-intake",
|
|
disabled: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO,
|
|
label:
|
|
!!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO ? (
|
|
t("jobs.actions.intake")
|
|
) : (
|
|
<Link to={`/manage/jobs/${job.id}/intake`}>{t("jobs.actions.intake")}</Link>
|
|
)
|
|
},
|
|
{
|
|
key: "deliver",
|
|
id: "job-actions-deliver",
|
|
disabled: !jobInProduction || jobRO,
|
|
label: !jobInProduction ? (
|
|
t("jobs.actions.deliver")
|
|
) : (
|
|
<Link to={`/manage/jobs/${job.id}/deliver`}>{t("jobs.actions.deliver")}</Link>
|
|
)
|
|
},
|
|
{
|
|
key: "checklist",
|
|
id: "job-actions-checklist",
|
|
disabled: !job.converted,
|
|
label: <Link to={`/manage/jobs/${job.id}/checklist`}>{t("jobs.actions.viewchecklist")}</Link>
|
|
}
|
|
],
|
|
rome: "USE_IMEX",
|
|
promanager: [
|
|
{
|
|
key: "toggleproduction",
|
|
id: "job-actions-toggleproduction",
|
|
disabled: !job.converted || jobRO,
|
|
label: <JobsDetailHeaderActionsToggleProduction job={job} refetch={refetch} />
|
|
}
|
|
]
|
|
}),
|
|
...(InstanceRenderManager({
|
|
imex: true,
|
|
rome: "USE_IMEX",
|
|
promanager: HasFeatureAccess({ featureName: "timetickets", bodyshop })
|
|
})
|
|
? [
|
|
{
|
|
key: "entertimetickets",
|
|
id: "job-actions-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
|
|
}
|
|
});
|
|
}
|
|
}
|
|
]
|
|
: [])
|
|
];
|
|
|
|
if (bodyshop.md_tasks_presets.enable_tasks) {
|
|
menuItems.push({
|
|
key: "claimtimetickettasks",
|
|
id: "job-actions-claimtimetickettasks",
|
|
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
|
onClick: () => {
|
|
setTimeTicketTaskContext({
|
|
actions: {},
|
|
context: { jobid: job.id }
|
|
});
|
|
},
|
|
label: t("timetickets.actions.claimtasks")
|
|
});
|
|
}
|
|
|
|
menuItems.push({
|
|
key: "enterpayments",
|
|
id: "job-actions-enterpayments",
|
|
disabled: !job.converted,
|
|
label: t("menus.header.enterpayment"),
|
|
onClick: () => {
|
|
logImEXEvent("job_header_enter_payment");
|
|
|
|
setPaymentContext({
|
|
actions: {},
|
|
context: { jobid: job.id }
|
|
});
|
|
}
|
|
});
|
|
|
|
if (ImEXPay.treatment === "on") {
|
|
menuItems.push({
|
|
key: "entercardpayments",
|
|
id: "job-actions-entercardpayments",
|
|
disabled: !job.converted,
|
|
label: t("menus.header.entercardpayment"),
|
|
onClick: () => {
|
|
logImEXEvent("job_header_enter_card_payment");
|
|
|
|
setCardPaymentContext({
|
|
actions: { refetch },
|
|
context: { jobid: job.id }
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
if (HasFeatureAccess({ featureName: "courtesycars", bodyshop })) {
|
|
menuItems.push({
|
|
key: "cccontract",
|
|
id: "job-actions-cccontract",
|
|
disabled: jobRO || !job.converted,
|
|
label: (
|
|
<Link state={{ jobId: job.id }} to="/manage/courtesycars/contracts/new">
|
|
{t("menus.jobsactions.newcccontract")}
|
|
</Link>
|
|
)
|
|
});
|
|
}
|
|
|
|
menuItems.push({
|
|
key: "createtask",
|
|
id: "job-actions-createtask",
|
|
label: t("menus.header.create_task"),
|
|
onClick: () =>
|
|
setTaskUpsertContext({
|
|
actions: {},
|
|
context: { jobid: job.id }
|
|
})
|
|
});
|
|
|
|
menuItems.push(
|
|
job.inproduction
|
|
? {
|
|
key: "removefromproduction",
|
|
id: "job-actions-removefromproduction",
|
|
disabled: !job.converted,
|
|
label: t("jobs.actions.removefromproduction"),
|
|
onClick: () => AddToProduction(client, job.id, refetch, true)
|
|
}
|
|
: {
|
|
key: "addtoproduction",
|
|
id: "job-actions-addtoproduction",
|
|
disabled: !job.converted,
|
|
label: t("jobs.actions.addtoproduction"),
|
|
onClick: () => AddToProduction(client, job.id, refetch)
|
|
}
|
|
);
|
|
|
|
menuItems.push(
|
|
{
|
|
key: "togglesuspend",
|
|
id: "job-actions-togglesuspend",
|
|
onClick: handleSuspend,
|
|
label: job.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")
|
|
},
|
|
{
|
|
key: "toggleAlert",
|
|
onClick: handleAlertToggle,
|
|
id: "job-actions-togglealert",
|
|
label:
|
|
job.production_vars && job.production_vars.alert
|
|
? t("production.labels.alertoff")
|
|
: t("production.labels.alerton")
|
|
},
|
|
{
|
|
key: "dupe",
|
|
label: t("menus.jobsactions.duplicate"),
|
|
children: [
|
|
{
|
|
key: "duplicate",
|
|
id: "job-actions-duplicate",
|
|
label: (
|
|
<Popconfirm
|
|
title={t("jobs.labels.duplicateconfirm")}
|
|
okText="Yes"
|
|
cancelText="No"
|
|
onClick={(e) => e.stopPropagation()}
|
|
onConfirm={handleDuplicate}
|
|
getPopupContainer={(trigger) => trigger.parentNode}
|
|
>
|
|
{t("menus.jobsactions.duplicate")}
|
|
</Popconfirm>
|
|
)
|
|
},
|
|
{
|
|
key: "duplicatenolines",
|
|
id: "job-actions-duplicatenolines",
|
|
label: (
|
|
<Popconfirm
|
|
title={t("jobs.labels.duplicateconfirm")}
|
|
okText="Yes"
|
|
cancelText="No"
|
|
onClick={(e) => e.stopPropagation()}
|
|
onConfirm={handleDuplicateConfirm}
|
|
getPopupContainer={(trigger) => trigger.parentNode}
|
|
>
|
|
{t("menus.jobsactions.duplicatenolines")}
|
|
</Popconfirm>
|
|
)
|
|
}
|
|
]
|
|
},
|
|
...(InstanceRenderManager({
|
|
imex: true,
|
|
rome: true,
|
|
promanager: HasFeatureAccess({ featureName: "bills", bodyshop })
|
|
})
|
|
? [
|
|
{
|
|
key: "postbills",
|
|
id: "job-actions-postbills",
|
|
disabled: !job.converted,
|
|
label: t("jobs.actions.postbills"),
|
|
onClick: () => {
|
|
logImEXEvent("job_header_enter_bills");
|
|
|
|
setBillEnterContext({
|
|
actions: { refetch: refetch },
|
|
context: {
|
|
job: job
|
|
}
|
|
});
|
|
}
|
|
}
|
|
]
|
|
: []),
|
|
|
|
{
|
|
key: "addtopartsqueue",
|
|
id: "job-actions-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",
|
|
id: "job-actions-closejob",
|
|
disabled: !jobInPostProduction,
|
|
label: !jobInPostProduction ? (
|
|
t("menus.jobsactions.closejob")
|
|
) : (
|
|
<Link
|
|
to={{
|
|
pathname: `/manage/jobs/${job.id}/close`
|
|
}}
|
|
>
|
|
{t("menus.jobsactions.closejob")}
|
|
</Link>
|
|
)
|
|
},
|
|
{
|
|
key: "admin",
|
|
id: "job-actions-admin",
|
|
label: (
|
|
<Link
|
|
to={{
|
|
pathname: `/manage/jobs/${job.id}/admin`
|
|
}}
|
|
>
|
|
{t("menus.jobsactions.admin")}
|
|
</Link>
|
|
)
|
|
}
|
|
);
|
|
|
|
if (
|
|
InstanceRenderManager({
|
|
imex: true,
|
|
rome: true,
|
|
promanager: HasFeatureAccess({ featureName: "export", bodyshop })
|
|
})
|
|
) {
|
|
menuItems.push({
|
|
key: "exportcustdata",
|
|
id: "job-actions-exportcustdata",
|
|
disabled: !job.converted,
|
|
label: t("jobs.actions.exportcustdata"),
|
|
onClick: handleExportCustData
|
|
});
|
|
}
|
|
|
|
if (HasFeatureAccess({ featureName: "csi", bodyshop })) {
|
|
const children = [
|
|
{
|
|
key: "email",
|
|
id: "job-actions-email",
|
|
disabled: !!!job.ownr_ea,
|
|
label: t("general.labels.email"),
|
|
onClick: handleCreateCsi
|
|
},
|
|
{
|
|
key: "text",
|
|
id: "job-actions-text",
|
|
disabled: !!!job.ownr_ph1,
|
|
label: t("general.labels.text"),
|
|
onClick: handleCreateCsi
|
|
},
|
|
{
|
|
key: "generate",
|
|
id: "job-actions-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: (
|
|
<Link to={`/manage/shop/csi?responseid=${item.id}`}>
|
|
<DateTimeFormatter>{item.completedon}</DateTimeFormatter>
|
|
</Link>
|
|
)
|
|
}
|
|
: {
|
|
key: idx,
|
|
onClick: () => {
|
|
navigator.clipboard.writeText(`${window.location.protocol}//${window.location.host}/csi/${item.id}`);
|
|
},
|
|
label: t("general.actions.copylink")
|
|
};
|
|
})
|
|
);
|
|
}
|
|
menuItems.push({
|
|
key: "sendcsi",
|
|
id: "job-actions-sendcsi",
|
|
label: t("jobs.actions.sendcsi"),
|
|
disabled: !job.converted,
|
|
children
|
|
});
|
|
}
|
|
|
|
menuItems.push({
|
|
key: "jobcosting",
|
|
id: "job-actions-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({
|
|
key: "deletejob",
|
|
id: "job-actions-deletejob",
|
|
label: (
|
|
<Popconfirm
|
|
title={t("jobs.labels.deleteconfirm")}
|
|
okText={t("general.labels.yes")}
|
|
cancelText={t("general.labels.no")}
|
|
onClick={(e) => e.stopPropagation()}
|
|
onConfirm={handleDeleteJob}
|
|
>
|
|
{t("menus.jobsactions.deletejob")}
|
|
</Popconfirm>
|
|
)
|
|
});
|
|
}
|
|
|
|
menuItems.push({
|
|
key: "manualevent",
|
|
id: "job-actions-manualevent",
|
|
onClick: (e) => {
|
|
setVisibility(true);
|
|
},
|
|
label: t("appointments.labels.manualevent")
|
|
});
|
|
|
|
if (!jobRO && job.converted) {
|
|
menuItems.push({
|
|
key: "voidjob",
|
|
id: "job-actions-voidjob",
|
|
label: (
|
|
<RbacWrapper action="jobs:void" noauth>
|
|
<Popconfirm
|
|
title={t("jobs.labels.voidjob")}
|
|
okText="Yes"
|
|
cancelText="No"
|
|
onClick={(e) => e.stopPropagation()}
|
|
onConfirm={handleVoidJob}
|
|
>
|
|
{t("menus.jobsactions.void")}
|
|
</Popconfirm>
|
|
</RbacWrapper>
|
|
)
|
|
});
|
|
}
|
|
|
|
const menu = {
|
|
items: menuItems,
|
|
key: "popovermenu"
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Modal
|
|
title={t("menus.jobsactions.cancelallappointments")}
|
|
open={isCancelScheduleModalVisible}
|
|
onOk={handleCancelScheduleOK}
|
|
onCancel={handleCancelScheduleModalCancel}
|
|
footer={[
|
|
<Button form="cancelScheduleForm" key="back" onClick={handleCancelScheduleModalCancel}>
|
|
{t("general.actions.cancel")}
|
|
</Button>,
|
|
<Button
|
|
form="cancelScheduleForm"
|
|
htmlType="submit"
|
|
key="submit"
|
|
type="primary"
|
|
loading={loading}
|
|
onClick={handleCancelScheduleOK}
|
|
>
|
|
{t("appointments.actions.cancel")}
|
|
</Button>
|
|
]}
|
|
>
|
|
<Form
|
|
layout="vertical"
|
|
id="cancelScheduleForm"
|
|
onFinish={(s) => {
|
|
console.log(s);
|
|
handleLostSaleFinish(s);
|
|
}}
|
|
>
|
|
<Form.Item
|
|
name="lost_sale_reason"
|
|
label={t("jobs.fields.lost_sale_reason")}
|
|
rules={[
|
|
{
|
|
required: true
|
|
//message: t("general.validation.required"),
|
|
}
|
|
]}
|
|
>
|
|
<Select
|
|
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
|
label: lsr,
|
|
value: lsr
|
|
}))}
|
|
/>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
<Dropdown menu={menu} trigger={["click"]} key="changestatus">
|
|
<Button>
|
|
<span>{t("general.labels.actions")}</span>
|
|
<DownCircleFilled />
|
|
</Button>
|
|
</Dropdown>
|
|
<Popover content={popOverContent} open={visibility} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailHeaderActions);
|