Fix / Merge / Rewrite Job Details Header Actions Menu
Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
@@ -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 = (
|
|
||||||
<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 handleClick = (e) => {
|
|
||||||
setVisibility(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO - Client Update - Why is this a menu item?
|
|
||||||
return (
|
|
||||||
<Popover content={overlay} open={visibility}>
|
|
||||||
<Menu.Item {...props} onClick={handleClick}>
|
|
||||||
{t("appointments.labels.manualevent")}
|
|
||||||
</Menu.Item>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,37 +1,32 @@
|
|||||||
import {DownCircleFilled} from "@ant-design/icons";
|
import {DownCircleFilled} from "@ant-design/icons";
|
||||||
import {useApolloClient, useMutation} from "@apollo/client";
|
import {useApolloClient, useMutation} from "@apollo/client";
|
||||||
import {
|
import {Button, Card, Dropdown, Form, Input, notification, Popconfirm, Popover, Select, Space,} from "antd";
|
||||||
Button,
|
import React, {useMemo, useState} from "react";
|
||||||
Dropdown,
|
|
||||||
Form,
|
|
||||||
Menu,
|
|
||||||
Popconfirm,
|
|
||||||
Popover,
|
|
||||||
Select,
|
|
||||||
notification,
|
|
||||||
} from "antd";
|
|
||||||
import React, { useMemo } from "react";
|
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import {Link, useNavigate} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import {createStructuredSelector} from "reselect";
|
import {createStructuredSelector} from "reselect";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
|
||||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID } from "../../graphql/appointments.queries";
|
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 {DELETE_JOB, UPDATE_JOB, VOID_JOB} from "../../graphql/jobs.queries";
|
||||||
import {insertAuditTrail} from "../../redux/application/application.actions";
|
import {insertAuditTrail} from "../../redux/application/application.actions";
|
||||||
import {selectJobReadOnly} from "../../redux/application/application.selectors";
|
import {selectJobReadOnly} from "../../redux/application/application.selectors";
|
||||||
import {setModalContext} from "../../redux/modals/modals.actions";
|
import {setModalContext} from "../../redux/modals/modals.actions";
|
||||||
import {
|
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||||
selectBodyshop,
|
|
||||||
selectCurrentUser,
|
|
||||||
} from "../../redux/user/user.selectors";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
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 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 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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -54,32 +49,27 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(setModalContext({context: context, modal: "cardPayment"})),
|
dispatch(setModalContext({context: context, modal: "cardPayment"})),
|
||||||
insertAuditTrail: ({jobid, operation}) =>
|
insertAuditTrail: ({jobid, operation}) =>
|
||||||
dispatch(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({
|
export function JobsDetailHeaderActions({job, bodyshop, currentUser, refetch, setScheduleContext, setBillEnterContext, setPaymentContext, setJobCostingContext, jobRO, setTimeTicketContext, setCardPaymentContext, insertAuditTrail, setEmailOptions, openChatByPhone, setMessage }) {
|
||||||
job,
|
|
||||||
bodyshop,
|
|
||||||
currentUser,
|
|
||||||
refetch,
|
|
||||||
setScheduleContext,
|
|
||||||
setBillEnterContext,
|
|
||||||
setPaymentContext,
|
|
||||||
setJobCostingContext,
|
|
||||||
jobRO,
|
|
||||||
setTimeTicketContext,
|
|
||||||
setCardPaymentContext,
|
|
||||||
insertAuditTrail,
|
|
||||||
}) {
|
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
||||||
const [deleteJob] = useMutation(DELETE_JOB);
|
const [deleteJob] = useMutation(DELETE_JOB);
|
||||||
|
const [insertCsi] = useMutation(INSERT_CSI);
|
||||||
const [updateJob] = useMutation(UPDATE_JOB);
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
const [voidJob] = useMutation(VOID_JOB);
|
const [voidJob] = useMutation(VOID_JOB);
|
||||||
const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
|
const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
|
||||||
const jobInProduction = useMemo(() => {
|
const jobInProduction = useMemo(() => {
|
||||||
return bodyshop.md_ro_statuses.production_statuses.includes(job.status);
|
return bodyshop.md_ro_statuses.production_statuses.includes(job.status);
|
||||||
}, [job, bodyshop.md_ro_statuses.production_statuses]);
|
}, [job, bodyshop.md_ro_statuses.production_statuses]);
|
||||||
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
|
||||||
const jobInPreProduction = useMemo(() => {
|
const jobInPreProduction = useMemo(() => {
|
||||||
return bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status);
|
return bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status);
|
||||||
@@ -91,6 +81,248 @@ export function JobsDetailHeaderActions({
|
|||||||
);
|
);
|
||||||
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setVisibility(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
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 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) => {
|
const handleAlertToggle = (e) => {
|
||||||
logImEXEvent("production_toggle_alert");
|
logImEXEvent("production_toggle_alert");
|
||||||
//e.stopPropagation();
|
//e.stopPropagation();
|
||||||
@@ -131,13 +363,99 @@ export function JobsDetailHeaderActions({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusmenu = (
|
const overlay = (
|
||||||
<Menu key="popovermenu">
|
<Card>
|
||||||
<Menu.Item
|
<div>
|
||||||
disabled={!jobInPreProduction || !job.converted || jobRO}
|
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
||||||
onClick={() => {
|
<Form.Item
|
||||||
logImEXEvent("job_header_schedule");
|
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 = [
|
||||||
|
{
|
||||||
|
disabled: !jobInPreProduction || !job.converted || jobRO,
|
||||||
|
label: t("jobs.actions.schedule"),
|
||||||
|
onClick: () => {
|
||||||
|
logImEXEvent("job_header_schedule");
|
||||||
setScheduleContext({
|
setScheduleContext({
|
||||||
actions: {refetch: refetch},
|
actions: {refetch: refetch},
|
||||||
context: {
|
context: {
|
||||||
@@ -146,14 +464,11 @@ export function JobsDetailHeaderActions({
|
|||||||
alt_transport: job.alt_transport,
|
alt_transport: job.alt_transport,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
},
|
||||||
>
|
},
|
||||||
{t("jobs.actions.schedule")}
|
{
|
||||||
</Menu.Item>
|
disabled: job.status !== bodyshop.md_ro_statuses.default_scheduled,
|
||||||
<Menu.Item
|
label: job.status !== bodyshop.md_ro_statuses.default_scheduled ? (
|
||||||
disabled={job.status !== bodyshop.md_ro_statuses.default_scheduled}
|
|
||||||
>
|
|
||||||
{job.status !== bodyshop.md_ro_statuses.default_scheduled ? (
|
|
||||||
t("menus.jobsactions.cancelallappointments")
|
t("menus.jobsactions.cancelallappointments")
|
||||||
) : (
|
) : (
|
||||||
<Popover
|
<Popover
|
||||||
@@ -185,7 +500,7 @@ export function JobsDetailHeaderActions({
|
|||||||
operation:
|
operation:
|
||||||
AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -219,48 +534,39 @@ export function JobsDetailHeaderActions({
|
|||||||
>
|
>
|
||||||
{t("menus.jobsactions.cancelallappointments")}
|
{t("menus.jobsactions.cancelallappointments")}
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)
|
||||||
</Menu.Item>
|
},
|
||||||
<Menu.Item
|
{
|
||||||
disabled={
|
disabled: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO,
|
||||||
!!job.intakechecklist ||
|
label: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO ? (
|
||||||
!jobInPreProduction ||
|
|
||||||
!job.converted ||
|
|
||||||
jobRO
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!!job.intakechecklist ||
|
|
||||||
!jobInPreProduction ||
|
|
||||||
!job.converted ||
|
|
||||||
jobRO ? (
|
|
||||||
t("jobs.actions.intake")
|
t("jobs.actions.intake")
|
||||||
) : (
|
) : (
|
||||||
<Link to={`/manage/jobs/${job.id}/intake`}>
|
<Link to={`/manage/jobs/${job.id}/intake`}>
|
||||||
{t("jobs.actions.intake")}
|
{t("jobs.actions.intake")}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)
|
||||||
</Menu.Item>
|
},
|
||||||
<Menu.Item disabled={!jobInProduction || jobRO}>
|
{
|
||||||
{!jobInProduction ? (
|
disabled: !jobInProduction || jobRO,
|
||||||
|
label: !jobInProduction ? (
|
||||||
t("jobs.actions.deliver")
|
t("jobs.actions.deliver")
|
||||||
) : (
|
) : (
|
||||||
<Link to={`/manage/jobs/${job.id}/deliver`}>
|
<Link to={`/manage/jobs/${job.id}/deliver`}>
|
||||||
{t("jobs.actions.deliver")}
|
{t("jobs.actions.deliver")}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)
|
||||||
</Menu.Item>
|
},
|
||||||
<Menu.Item disabled={!job.converted}>
|
{
|
||||||
<Link to={`/manage/jobs/${job.id}/checklist`}>
|
disabled: !job.converted,
|
||||||
|
label: <Link to={`/manage/jobs/${job.id}/checklist`}>
|
||||||
{t("jobs.actions.viewchecklist")}
|
{t("jobs.actions.viewchecklist")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
},
|
||||||
<Menu.Item
|
{
|
||||||
key="entertimetickets"
|
key: "entertimetickets",
|
||||||
disabled={
|
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||||
!job.converted ||
|
label: t("timetickets.actions.enter"),
|
||||||
(!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced)
|
onClick: () => {
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
logImEXEvent("job_header_enter_time_ticekts");
|
logImEXEvent("job_header_enter_time_ticekts");
|
||||||
|
|
||||||
setTimeTicketContext({
|
setTimeTicketContext({
|
||||||
@@ -272,38 +578,38 @@ export function JobsDetailHeaderActions({
|
|||||||
: currentUser.email,
|
: currentUser.email,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}
|
||||||
>
|
},
|
||||||
{t("timetickets.actions.enter")}
|
{
|
||||||
</Menu.Item>
|
key: 'enterpayments',
|
||||||
<Menu.Item
|
disabled: !job.converted,
|
||||||
key="enterpayments"
|
label: t("menus.header.enterpayment"),
|
||||||
disabled={!job.converted}
|
onClick: () => {
|
||||||
onClick={() => {
|
|
||||||
logImEXEvent("job_header_enter_payment");
|
logImEXEvent("job_header_enter_payment");
|
||||||
|
|
||||||
setPaymentContext({
|
setPaymentContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
context: {jobid: job.id},
|
context: {jobid: job.id},
|
||||||
});
|
});
|
||||||
}}
|
}
|
||||||
>
|
},
|
||||||
{t("menus.header.enterpayment")}
|
{
|
||||||
</Menu.Item>
|
key: 'entercardpayments',
|
||||||
<Menu.Item
|
disabled: !job.converted,
|
||||||
key="entercardpayments"
|
label: t("menus.header.entercardpayment"),
|
||||||
disabled={!job.converted}
|
onClick: () => {
|
||||||
onClick={() => {
|
logImEXEvent("job_header_enter_card_payment");
|
||||||
|
|
||||||
setCardPaymentContext({
|
setCardPaymentContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
context: {jobid: job.id},
|
context: {jobid: job.id},
|
||||||
});
|
});
|
||||||
}}
|
}
|
||||||
>
|
},
|
||||||
{t("menus.header.entercardpayment")}
|
{
|
||||||
</Menu.Item>
|
key: 'cccontract',
|
||||||
<Menu.Item key="cccontract" disabled={jobRO || !job.converted}>
|
disabled: jobRO || !job.converted,
|
||||||
<Link
|
label: <Link
|
||||||
to={{
|
to={{
|
||||||
pathname: "/manage/courtesycars/contracts/new",
|
pathname: "/manage/courtesycars/contracts/new",
|
||||||
state: {jobId: job.id},
|
state: {jobId: job.id},
|
||||||
@@ -311,37 +617,46 @@ export function JobsDetailHeaderActions({
|
|||||||
>
|
>
|
||||||
{t("menus.jobsactions.newcccontract")}
|
{t("menus.jobsactions.newcccontract")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
}
|
||||||
{job.inproduction ? (
|
];
|
||||||
<Menu.Item
|
|
||||||
key="addtoproduction"
|
menuItems.push(
|
||||||
disabled={!job.converted}
|
job.inproduction ?
|
||||||
onClick={() => AddToProduction(client, job.id, refetch, true)}
|
{
|
||||||
>
|
key: 'addtoproduction',
|
||||||
{t("jobs.actions.removefromproduction")}
|
disabled: !job.converted,
|
||||||
</Menu.Item>
|
label: t("jobs.actions.removefromproduction"),
|
||||||
) : (
|
onClick: () => AddToProduction(client, job.id, refetch, true)
|
||||||
<Menu.Item
|
} :
|
||||||
key="addtoproduction"
|
{
|
||||||
disabled={!job.converted}
|
key: 'addtoproduction',
|
||||||
onClick={() => AddToProduction(client, job.id, refetch)}
|
disabled: !job.converted,
|
||||||
>
|
label: t("jobs.actions.addtoproduction"),
|
||||||
{t("jobs.actions.addtoproduction")}
|
onClick: () => AddToProduction(client, job.id, refetch)
|
||||||
</Menu.Item>
|
}
|
||||||
)}
|
);
|
||||||
<Menu.Item key="togglesuspend" onClick={handleSuspend}>
|
|
||||||
{job.suspended
|
menuItems.push(
|
||||||
|
{
|
||||||
|
key: 'togglesuspend',
|
||||||
|
onClick: handleSuspend,
|
||||||
|
label: job.suspended
|
||||||
? t("production.actions.unsuspend")
|
? t("production.actions.unsuspend")
|
||||||
: t("production.actions.suspend")}
|
: t("production.actions.suspend")
|
||||||
</Menu.Item>
|
},
|
||||||
<Menu.Item key="toggleAlert" onClick={handleAlertToggle}>
|
{
|
||||||
{job.production_vars && job.production_vars.alert
|
key: 'toggleAlert',
|
||||||
|
onClick: handleAlertToggle,
|
||||||
|
label: job.production_vars && job.production_vars.alert
|
||||||
? t("production.labels.alertoff")
|
? t("production.labels.alertoff")
|
||||||
: t("production.labels.alerton")}
|
: t("production.labels.alerton")
|
||||||
</Menu.Item>
|
},
|
||||||
<Menu.SubMenu key="dupe" title={t("menus.jobsactions.duplicate")}>
|
{
|
||||||
<Menu.Item>
|
key: 'dupe',
|
||||||
<Popconfirm
|
label: t("menus.jobsactions.duplicate"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: <Popconfirm
|
||||||
title={t("jobs.labels.duplicateconfirm")}
|
title={t("jobs.labels.duplicateconfirm")}
|
||||||
okText="Yes"
|
okText="Yes"
|
||||||
cancelText="No"
|
cancelText="No"
|
||||||
@@ -364,9 +679,9 @@ export function JobsDetailHeaderActions({
|
|||||||
>
|
>
|
||||||
{t("menus.jobsactions.duplicate")}
|
{t("menus.jobsactions.duplicate")}
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Menu.Item>
|
},
|
||||||
<Menu.Item>
|
{
|
||||||
<Popconfirm
|
label: <Popconfirm
|
||||||
title={t("jobs.labels.duplicateconfirm")}
|
title={t("jobs.labels.duplicateconfirm")}
|
||||||
okText="Yes"
|
okText="Yes"
|
||||||
cancelText="No"
|
cancelText="No"
|
||||||
@@ -388,13 +703,14 @@ export function JobsDetailHeaderActions({
|
|||||||
>
|
>
|
||||||
{t("menus.jobsactions.duplicatenolines")}
|
{t("menus.jobsactions.duplicatenolines")}
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Menu.Item>
|
}
|
||||||
</Menu.SubMenu>
|
]
|
||||||
|
},
|
||||||
<Menu.Item
|
{
|
||||||
key="postbills"
|
key: 'postbills',
|
||||||
disabled={!job.converted}
|
disabled: !job.converted,
|
||||||
onClick={() => {
|
label: t("jobs.actions.postbills"),
|
||||||
|
onClick: () => {
|
||||||
logImEXEvent("job_header_enter_bills");
|
logImEXEvent("job_header_enter_bills");
|
||||||
|
|
||||||
setBillEnterContext({
|
setBillEnterContext({
|
||||||
@@ -403,14 +719,13 @@ export function JobsDetailHeaderActions({
|
|||||||
job: job,
|
job: job,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}
|
||||||
>
|
},
|
||||||
{t("jobs.actions.postbills")}
|
{
|
||||||
</Menu.Item>
|
key: 'addtopartsqueue',
|
||||||
<Menu.Item
|
disabled: !job.converted || !jobInProduction || jobRO,
|
||||||
key="addtopartsqueue"
|
label: t("jobs.actions.addtopartsqueue"),
|
||||||
disabled={!job.converted || !jobInProduction || jobRO}
|
onClick: async () => {
|
||||||
onClick={async () => {
|
|
||||||
const result = await updateJob({
|
const result = await updateJob({
|
||||||
variables: {
|
variables: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
@@ -429,12 +744,12 @@ export function JobsDetailHeaderActions({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
>
|
},
|
||||||
{t("jobs.actions.addtopartsqueue")}
|
{
|
||||||
</Menu.Item>
|
key: 'closejob',
|
||||||
<Menu.Item disabled={!jobInPostProduction} key="closejob">
|
disabled: !jobInPostProduction,
|
||||||
{!jobInPostProduction ? (
|
label: !jobInPostProduction ? (
|
||||||
t("menus.jobsactions.closejob")
|
t("menus.jobsactions.closejob")
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
@@ -444,37 +759,107 @@ export function JobsDetailHeaderActions({
|
|||||||
>
|
>
|
||||||
{t("menus.jobsactions.closejob")}
|
{t("menus.jobsactions.closejob")}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)
|
||||||
</Menu.Item>
|
},
|
||||||
<Menu.Item key="admin">
|
{
|
||||||
<Link
|
key: 'admin',
|
||||||
|
label: <Link
|
||||||
to={{
|
to={{
|
||||||
pathname: `/manage/jobs/${job.id}/admin`,
|
pathname: `/manage/jobs/${job.id}/admin`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("menus.jobsactions.admin")}
|
{t("menus.jobsactions.admin")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
}
|
||||||
<JobsDetailHeaderActionsExportcustdataComponent job={job} />
|
);
|
||||||
<JobsDetaiLheaderCsi job={job} />
|
|
||||||
<Menu.Item
|
menuItems.push(
|
||||||
key="jobcosting"
|
{
|
||||||
disabled={!job.converted}
|
key: 'exportcustdata',
|
||||||
onClick={() => {
|
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: <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',
|
||||||
|
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");
|
logImEXEvent("job_header_job_costing");
|
||||||
|
|
||||||
setJobCostingContext({
|
setJobCostingContext({
|
||||||
actions: {refetch: refetch},
|
actions: {refetch: refetch},
|
||||||
context: {
|
context: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}
|
||||||
>
|
}
|
||||||
{t("jobs.labels.jobcosting")}
|
);
|
||||||
</Menu.Item>
|
|
||||||
{job && !job.converted && (
|
if (job && !job.converted) {
|
||||||
<Menu.Item>
|
menuItems.push(
|
||||||
<Popconfirm
|
{
|
||||||
|
label: <Popconfirm
|
||||||
title={t("jobs.labels.deleteconfirm")}
|
title={t("jobs.labels.deleteconfirm")}
|
||||||
okText={t("general.labels.yes")}
|
okText={t("general.labels.yes")}
|
||||||
cancelText={t("general.labels.no")}
|
cancelText={t("general.labels.no")}
|
||||||
@@ -501,12 +886,22 @@ export function JobsDetailHeaderActions({
|
|||||||
>
|
>
|
||||||
{t("menus.jobsactions.deletejob")}
|
{t("menus.jobsactions.deletejob")}
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Menu.Item>
|
}
|
||||||
)}
|
);
|
||||||
<JobsDetailHeaderActionsAddevent jobid={job.id} />
|
}
|
||||||
{!jobRO && job.converted && (
|
|
||||||
<RbacWrapper action="jobs:void" noauth>
|
menuItems.push(
|
||||||
<Menu.Item>
|
{
|
||||||
|
onClick: (e) => {
|
||||||
|
setVisibility(true);
|
||||||
|
},
|
||||||
|
label: t("appointments.labels.manualevent")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!jobRO && job.converted) {
|
||||||
|
menuItems.push({
|
||||||
|
label: <RbacWrapper action="jobs:void" noauth>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("jobs.labels.voidjob")}
|
title={t("jobs.labels.voidjob")}
|
||||||
okText="Yes"
|
okText="Yes"
|
||||||
@@ -554,21 +949,28 @@ export function JobsDetailHeaderActions({
|
|||||||
>
|
>
|
||||||
{t("menus.jobsactions.void")}
|
{t("menus.jobsactions.void")}
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Menu.Item>
|
|
||||||
</RbacWrapper>
|
</RbacWrapper>
|
||||||
)}
|
});
|
||||||
</Menu>
|
}
|
||||||
);
|
|
||||||
|
const menu = {
|
||||||
|
items: menuItems,
|
||||||
|
key: 'popovermenu'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={statusmenu} trigger={["click"]} key="changestatus">
|
<>
|
||||||
|
<Dropdown menu={menu} trigger={["click"]} key="changestatus">
|
||||||
<Button>
|
<Button>
|
||||||
<span>{t("general.labels.actions")}</span>
|
<span>{t("general.labels.actions")}</span>
|
||||||
|
|
||||||
<DownCircleFilled/>
|
<DownCircleFilled/>
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<Popover content={overlay} open={visibility} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<Menu.SubMenu
|
|
||||||
key="sendcsi"
|
|
||||||
title={t("jobs.actions.sendcsi")}
|
|
||||||
disabled={!job.converted}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Menu.Item
|
|
||||||
onClick={handleCreateCsi}
|
|
||||||
key="email"
|
|
||||||
disabled={!!!job.ownr_ea}
|
|
||||||
>
|
|
||||||
{t("general.labels.email")}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
|
||||||
onClick={handleCreateCsi}
|
|
||||||
key="text"
|
|
||||||
disabled={!!!job.ownr_ph1}
|
|
||||||
>
|
|
||||||
{t("general.labels.text")}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item
|
|
||||||
onClick={handleCreateCsi}
|
|
||||||
key="generate"
|
|
||||||
disabled={job.csiinvites && job.csiinvites.length > 0}
|
|
||||||
>
|
|
||||||
{t("jobs.actions.generatecsi")}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Divider />
|
|
||||||
{job.csiinvites.map((item, idx) => {
|
|
||||||
return item.completedon ? (
|
|
||||||
<Menu.Item key={idx}>
|
|
||||||
<Link to={`/manage/shop/csi?responseid=${item.id}`}>
|
|
||||||
<DateTimeFormatter>{item.completedon}</DateTimeFormatter>
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
) : (
|
|
||||||
<Menu.Item
|
|
||||||
key={idx}
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
`${window.location.protocol}//${window.location.host}/csi/${item.id}`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.actions.copylink")}
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Menu.SubMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(JobsDetailHeaderCsi);
|
|
||||||
@@ -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 (
|
|
||||||
<Menu.Item
|
|
||||||
{...props}
|
|
||||||
onClick={handleExportCustData}
|
|
||||||
key="exportcustdata"
|
|
||||||
disabled={!job.converted}
|
|
||||||
>
|
|
||||||
{t("jobs.actions.exportcustdata")}
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(JobsDetailHeaderActionexportCustomerData);
|
|
||||||
Reference in New Issue
Block a user