Fix / Merge / Rewrite Job Details Header Actions Menu

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-01-04 15:51:44 -05:00
parent e43bfe0d3a
commit 351d6f274b
4 changed files with 948 additions and 1067 deletions

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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);