IO-2932-Scheduling-Lag-on-AIO:
Bump React-Big-Calendar Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -11,55 +11,61 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ScheduleEventColor({ bodyshop, event }) {
|
export function ScheduleEventColor({ bodyshop, event }) {
|
||||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onClick = async ({ key }) => {
|
const onClick = useCallback(
|
||||||
const result = await updateAppointment({
|
async ({ key }) => {
|
||||||
variables: {
|
const result = await updateAppointment({
|
||||||
appid: event.id,
|
variables: {
|
||||||
app: { color: key === "null" ? null : key }
|
appid: event.id,
|
||||||
}
|
app: { color: key === "null" ? null : key }
|
||||||
});
|
}
|
||||||
|
|
||||||
if (!!!result.errors) {
|
|
||||||
notification["success"]({ message: t("appointments.successes.saved") });
|
|
||||||
} else {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("appointments.errors.saving", {
|
|
||||||
error: JSON.stringify(result.errors)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!result.errors) {
|
||||||
|
notification.success({ message: t("appointments.successes.saved") });
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: t("appointments.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[event.id, t, updateAppointment]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedColor = useMemo(() => {
|
||||||
|
if (event.color && bodyshop.appt_colors) {
|
||||||
|
const colorObj = bodyshop.appt_colors.find((color) => color.color.hex === event.color);
|
||||||
|
return colorObj?.label;
|
||||||
}
|
}
|
||||||
};
|
return null;
|
||||||
|
}, [event.color, bodyshop.appt_colors]);
|
||||||
|
|
||||||
const selectedColor =
|
const menu = useMemo(
|
||||||
event.color &&
|
() => ({
|
||||||
bodyshop.appt_colors &&
|
defaultSelectedKeys: [event.color],
|
||||||
bodyshop.appt_colors.filter((color) => color.color.hex === event.color)[0]?.label;
|
onClick: onClick,
|
||||||
|
items: [
|
||||||
const menu = {
|
...(bodyshop.appt_colors || []).map((color) => ({
|
||||||
defaultSelectedKeys: [event.color],
|
key: color.color.hex,
|
||||||
onClick: onClick,
|
label: color.label,
|
||||||
items: [
|
style: { color: color.color.hex }
|
||||||
...(bodyshop.appt_colors || []).map((color) => ({
|
})),
|
||||||
key: color.color.hex,
|
{ type: "divider" },
|
||||||
label: color.label,
|
{ key: "null", label: t("general.actions.clear") }
|
||||||
style: { color: color.color.hex }
|
]
|
||||||
})),
|
}),
|
||||||
{ type: "divider" },
|
[bodyshop.appt_colors, event.color, onClick, t]
|
||||||
{ key: "null", label: t("general.actions.clear") }
|
);
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={menu}>
|
<Dropdown menu={menu}>
|
||||||
<a href=" #" onClick={(e) => e.preventDefault()}>
|
<a href="#" onClick={(e) => e.preventDefault()}>
|
||||||
{selectedColor}
|
{selectedColor}
|
||||||
<DownOutlined />
|
<DownOutlined />
|
||||||
</a>
|
</a>
|
||||||
@@ -67,4 +73,4 @@ export function ScheduleEventColor({ bodyshop, event }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventColor);
|
export default connect(mapStateToProps)(React.memo(ScheduleEventColor));
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button, Divider, Dropdown, Form, Input, notification, Popover, Select,
|
|||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState, useCallback, 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, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -27,6 +27,7 @@ import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
@@ -44,301 +45,322 @@ export function ScheduleEventComponent({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const history = useNavigate();
|
const navigate = useNavigate();
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const location = useLocation();
|
||||||
|
const searchParams = queryString.parse(location.search);
|
||||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||||
const [title, setTitle] = useState(event.title);
|
const [title, setTitle] = useState(event.title);
|
||||||
const blockContent = (
|
|
||||||
<Space direction="vertical" wrap>
|
|
||||||
<Input
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => setTitle(e.currentTarget.value)}
|
|
||||||
onBlur={async () => {
|
|
||||||
await updateAppointment({
|
|
||||||
variables: {
|
|
||||||
appid: event.id,
|
|
||||||
app: {
|
|
||||||
title: title
|
|
||||||
}
|
|
||||||
},
|
|
||||||
optimisticResponse: {
|
|
||||||
update_appointments: {
|
|
||||||
__typename: "appointments_mutation_response",
|
|
||||||
returning: [
|
|
||||||
{
|
|
||||||
...event,
|
|
||||||
title: title,
|
|
||||||
__typename: "appointments"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button onClick={() => handleCancel({ id: event.id })} disabled={event.arrived}>
|
const handleTitleBlur = useCallback(async () => {
|
||||||
{t("appointments.actions.unblock")}
|
await updateAppointment({
|
||||||
</Button>
|
variables: {
|
||||||
</Space>
|
appid: event.id,
|
||||||
|
app: {
|
||||||
|
title: title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optimisticResponse: {
|
||||||
|
update_appointments: {
|
||||||
|
__typename: "appointments_mutation_response",
|
||||||
|
returning: [
|
||||||
|
{
|
||||||
|
...event,
|
||||||
|
title: title,
|
||||||
|
__typename: "appointments"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [updateAppointment, event, title]);
|
||||||
|
|
||||||
|
const handleUnblock = useCallback(() => {
|
||||||
|
handleCancel({ id: event.id });
|
||||||
|
}, [handleCancel, event.id]);
|
||||||
|
|
||||||
|
const handlePreviewClick = useCallback(() => {
|
||||||
|
navigate({
|
||||||
|
search: queryString.stringify({
|
||||||
|
...searchParams,
|
||||||
|
selected: event.job.id
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}, [navigate, searchParams, event.job.id]);
|
||||||
|
|
||||||
|
const handleSendEmailReminder = useCallback(() => {
|
||||||
|
const Template = TemplateList("job").appointment_reminder;
|
||||||
|
GenerateDocument(
|
||||||
|
{
|
||||||
|
name: Template.key,
|
||||||
|
variables: { id: event.job.id }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: event.job && event.job.ownr_ea,
|
||||||
|
subject: Template.subject
|
||||||
|
},
|
||||||
|
"e",
|
||||||
|
event.job && event.job.id
|
||||||
|
);
|
||||||
|
}, [event.job]);
|
||||||
|
|
||||||
|
const handleSendSMSReminder = useCallback(() => {
|
||||||
|
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
|
||||||
|
if (p && p.isValid()) {
|
||||||
|
openChatByPhone({
|
||||||
|
phone_num: p.formatInternational(),
|
||||||
|
jobid: event.job.id
|
||||||
|
});
|
||||||
|
setMessage(
|
||||||
|
t("appointments.labels.reminder", {
|
||||||
|
shopname: bodyshop.shopname,
|
||||||
|
date: dayjs(event.start).format("MM/DD/YYYY"),
|
||||||
|
time: dayjs(event.start).format("HH:mm a")
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setOpen(false);
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: t("messaging.error.invalidphone")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [event.job, openChatByPhone, setMessage, t, bodyshop.shopname, event.start, setOpen]);
|
||||||
|
|
||||||
|
const reminderMenuItems = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: "email",
|
||||||
|
label: t("general.labels.email"),
|
||||||
|
disabled: event.arrived,
|
||||||
|
onClick: handleSendEmailReminder
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "sms",
|
||||||
|
label: t("general.labels.sms"),
|
||||||
|
disabled: event.arrived || !bodyshop.messagingservicesid,
|
||||||
|
onClick: handleSendSMSReminder
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[t, event.arrived, handleSendEmailReminder, handleSendSMSReminder, bodyshop.messagingservicesid]
|
||||||
);
|
);
|
||||||
|
|
||||||
const popoverContent = (
|
const reminderMenu = useMemo(() => ({ items: reminderMenuItems }), [reminderMenuItems]);
|
||||||
<div style={{ maxWidth: "40vw" }}>
|
|
||||||
{!event.isintake ? (
|
|
||||||
<Space>
|
|
||||||
<strong>{event.title}</strong>
|
|
||||||
<ScheduleEventColor event={event} />
|
|
||||||
</Space>
|
|
||||||
) : (
|
|
||||||
<Space>
|
|
||||||
<strong>
|
|
||||||
<OwnerNameDisplay ownerObject={event.job} />
|
|
||||||
</strong>
|
|
||||||
<span style={{ margin: 4 }}>
|
|
||||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
|
||||||
(event.job && event.job.v_make_desc) || ""
|
|
||||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
|
||||||
</span>
|
|
||||||
<ScheduleEventColor event={event} />
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{event.job ? (
|
const handleCancelFormFinish = useCallback(
|
||||||
<div>
|
({ lost_sale_reason }) => {
|
||||||
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
|
handleCancel({ id: event.id, lost_sale_reason });
|
||||||
<DataLabel label={t("jobs.fields.clm_total")}>
|
},
|
||||||
<CurrencyFormatter>{(event.job && event.job.clm_total) || ""}</CurrencyFormatter>
|
[handleCancel, event.id]
|
||||||
</DataLabel>
|
);
|
||||||
<DataLabel hideIfNull label={t("jobs.fields.ins_co_nm")}>
|
|
||||||
{(event.job && event.job.ins_co_nm) || ""}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel hideIfNull label={t("jobs.fields.clm_no")}>
|
|
||||||
{(event.job && event.job.clm_no) || ""}
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.ownr_ea")}>{(event.job && event.job.ownr_ea) || ""}</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.ownr_ph1")}>
|
|
||||||
<ChatOpenButton phone={event.job && event.job.ownr_ph1} jobid={event.job.id} />
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.ownr_ph2")}>
|
|
||||||
<ChatOpenButton phone={event.job && event.job.ownr_ph2} jobid={event.job.id} />
|
|
||||||
</DataLabel>
|
|
||||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
|
||||||
{(event.job && event.job.alt_transport) || ""}
|
|
||||||
<ScheduleAtChange job={event && event.job} />
|
|
||||||
</DataLabel>
|
|
||||||
<ScheduleEventNote event={event} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>{event.note || ""}</div>
|
|
||||||
)}
|
|
||||||
<Divider />
|
|
||||||
<Space wrap>
|
|
||||||
{event.job ? (
|
|
||||||
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
|
|
||||||
<Button>{t("appointments.actions.viewjob")}</Button>
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
{event.job ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
history({
|
|
||||||
search: queryString.stringify({
|
|
||||||
...searchParams,
|
|
||||||
selected: event.job.id
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("appointments.actions.preview")}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{event.job ? (
|
|
||||||
<Dropdown
|
|
||||||
menu={{
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
key: "email",
|
|
||||||
label: t("general.labels.email"),
|
|
||||||
disabled: event.arrived,
|
|
||||||
onClick: () => {
|
|
||||||
const Template = TemplateList("job").appointment_reminder;
|
|
||||||
GenerateDocument(
|
|
||||||
{
|
|
||||||
name: Template.key,
|
|
||||||
variables: { id: event.job.id }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: event.job && event.job.ownr_ea,
|
|
||||||
subject: Template.subject
|
|
||||||
},
|
|
||||||
"e",
|
|
||||||
event.job && event.job.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "sms",
|
|
||||||
label: t("general.labels.sms"),
|
|
||||||
disabled: event.arrived || !bodyshop.messagingservicesid,
|
|
||||||
onClick: () => {
|
|
||||||
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
|
|
||||||
if (p && p.isValid()) {
|
|
||||||
openChatByPhone({
|
|
||||||
phone_num: p.formatInternational(),
|
|
||||||
jobid: event.job.id
|
|
||||||
});
|
|
||||||
setMessage(
|
|
||||||
t("appointments.labels.reminder", {
|
|
||||||
shopname: bodyshop.shopname,
|
|
||||||
date: dayjs(event.start).format("MM/DD/YYYY"),
|
|
||||||
time: dayjs(event.start).format("HH:mm a")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
setOpen(false);
|
|
||||||
} else {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("messaging.error.invalidphone")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button>{t("appointments.actions.sendreminder")}</Button>
|
|
||||||
</Dropdown>
|
|
||||||
) : null}
|
|
||||||
{event.arrived ? (
|
|
||||||
<Button
|
|
||||||
// onClick={() => handleCancel(event.id)}
|
|
||||||
disabled={event.arrived}
|
|
||||||
>
|
|
||||||
{t("appointments.actions.cancel")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Popover
|
|
||||||
trigger="click"
|
|
||||||
disabled={event.arrived}
|
|
||||||
content={
|
|
||||||
<Form
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={({ lost_sale_reason }) => {
|
|
||||||
handleCancel({ id: event.id, lost_sale_reason });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<Button htmlType="submit">{t("appointments.actions.cancel")}</Button>
|
|
||||||
</Form>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
// onClick={() => handleCancel(event.id)}
|
|
||||||
disabled={event.arrived}
|
|
||||||
>
|
|
||||||
{t("appointments.actions.cancel")}
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{event.isintake ? (
|
const handleRescheduleClick = useCallback(() => {
|
||||||
<Button
|
setOpen(false);
|
||||||
disabled={event.arrived}
|
setScheduleContext({
|
||||||
onClick={() => {
|
actions: { refetch: refetch },
|
||||||
setOpen(false);
|
context: {
|
||||||
setScheduleContext({
|
jobId: event.job.id,
|
||||||
actions: { refetch: refetch },
|
job: event.job,
|
||||||
context: {
|
previousEvent: event.id,
|
||||||
jobId: event.job.id,
|
color: event.color,
|
||||||
job: event.job,
|
alt_transport: event.job && event.job.alt_transport,
|
||||||
previousEvent: event.id,
|
note: event.note
|
||||||
color: event.color,
|
}
|
||||||
alt_transport: event.job && event.job.alt_transport,
|
});
|
||||||
note: event.note
|
}, [setOpen, setScheduleContext, refetch, event]);
|
||||||
}
|
|
||||||
});
|
const handleOpenChange = useCallback(
|
||||||
}}
|
(vis) => {
|
||||||
>
|
if (!event.vacation) setOpen(vis);
|
||||||
{t("appointments.actions.reschedule")}
|
},
|
||||||
</Button>
|
[event.vacation]
|
||||||
) : (
|
);
|
||||||
<ScheduleManualEvent event={event} />
|
|
||||||
)}
|
const blockContent = useMemo(
|
||||||
{event.isintake ? (
|
() => (
|
||||||
<Link
|
<Space direction="vertical" wrap>
|
||||||
to={{
|
<Input value={title} onChange={(e) => setTitle(e.currentTarget.value)} onBlur={handleTitleBlur} />
|
||||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
|
||||||
search: `?appointmentId=${event.id}`
|
<Button onClick={handleUnblock} disabled={event.arrived}>
|
||||||
}}
|
{t("appointments.actions.unblock")}
|
||||||
>
|
</Button>
|
||||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
),
|
||||||
|
[title, handleTitleBlur, handleUnblock, event.arrived, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const RegularEvent = event.isintake ? (
|
const popoverContent = useMemo(
|
||||||
<Space
|
() => (
|
||||||
wrap
|
<div style={{ maxWidth: "40vw" }}>
|
||||||
size="small"
|
{!event.isintake ? (
|
||||||
style={{
|
<Space>
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
<strong>{event.title}</strong>
|
||||||
}}
|
<ScheduleEventColor event={event} />
|
||||||
>
|
</Space>
|
||||||
{event.note && <AlertFilled className="production-alert" />}
|
) : (
|
||||||
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
<Space>
|
||||||
|
<strong>
|
||||||
|
<OwnerNameDisplay ownerObject={event.job} />
|
||||||
|
</strong>
|
||||||
|
<span style={{ margin: 4 }}>
|
||||||
|
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||||
|
(event.job && event.job.v_make_desc) || ""
|
||||||
|
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||||
|
</span>
|
||||||
|
<ScheduleEventColor event={event} />
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
|
||||||
<OwnerNameDisplay ownerObject={event.job} />
|
{event.job ? (
|
||||||
|
<div>
|
||||||
|
<DataLabel label={t("jobs.fields.ro_number")}>{(event.job && event.job.ro_number) || ""}</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.clm_total")}>
|
||||||
|
<CurrencyFormatter>{(event.job && event.job.clm_total) || ""}</CurrencyFormatter>
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel hideIfNull label={t("jobs.fields.ins_co_nm")}>
|
||||||
|
{(event.job && event.job.ins_co_nm) || ""}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel hideIfNull label={t("jobs.fields.clm_no")}>
|
||||||
|
{(event.job && event.job.clm_no) || ""}
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.ownr_ea")}>{(event.job && event.job.ownr_ea) || ""}</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.ownr_ph1")}>
|
||||||
|
<ChatOpenButton phone={event.job && event.job.ownr_ph1} jobid={event.job.id} />
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.ownr_ph2")}>
|
||||||
|
<ChatOpenButton phone={event.job && event.job.ownr_ph2} jobid={event.job.id} />
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||||
|
{(event.job && event.job.alt_transport) || ""}
|
||||||
|
<ScheduleAtChange job={event && event.job} />
|
||||||
|
</DataLabel>
|
||||||
|
<ScheduleEventNote event={event} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>{event.note || ""}</div>
|
||||||
|
)}
|
||||||
|
<Divider />
|
||||||
|
<Space wrap>
|
||||||
|
{event.job ? (
|
||||||
|
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
|
||||||
|
<Button>{t("appointments.actions.viewjob")}</Button>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
{event.job ? <Button onClick={handlePreviewClick}>{t("appointments.actions.preview")}</Button> : null}
|
||||||
|
{event.job ? (
|
||||||
|
<Dropdown menu={reminderMenu}>
|
||||||
|
<Button>{t("appointments.actions.sendreminder")}</Button>
|
||||||
|
</Dropdown>
|
||||||
|
) : null}
|
||||||
|
{event.arrived ? (
|
||||||
|
<Button disabled={event.arrived}>{t("appointments.actions.cancel")}</Button>
|
||||||
|
) : (
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
disabled={event.arrived}
|
||||||
|
content={
|
||||||
|
<Form layout="vertical" onFinish={handleCancelFormFinish}>
|
||||||
|
<Form.Item
|
||||||
|
name="lost_sale_reason"
|
||||||
|
label={t("jobs.fields.lost_sale_reason")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
||||||
|
label: lsr,
|
||||||
|
value: lsr
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Button htmlType="submit">{t("appointments.actions.cancel")}</Button>
|
||||||
|
</Form>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button disabled={event.arrived}>{t("appointments.actions.cancel")}</Button>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
{event.isintake ? (
|
||||||
(event.job && event.job.v_make_desc) || ""
|
<Button disabled={event.arrived} onClick={handleRescheduleClick}>
|
||||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
{t("appointments.actions.reschedule")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<ScheduleManualEvent event={event} />
|
||||||
|
)}
|
||||||
|
{event.isintake ? (
|
||||||
|
<Link
|
||||||
|
to={{
|
||||||
|
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||||
|
search: `?appointmentId=${event.id}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
event,
|
||||||
|
t,
|
||||||
|
handlePreviewClick,
|
||||||
|
reminderMenu,
|
||||||
|
bodyshop.md_lost_sale_reasons,
|
||||||
|
handleCancelFormFinish,
|
||||||
|
handleRescheduleClick
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
|
const RegularEvent = useMemo(
|
||||||
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
() =>
|
||||||
})`}
|
event.isintake ? (
|
||||||
|
<Space
|
||||||
|
wrap
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{event.note && <AlertFilled className="production-alert" />}
|
||||||
|
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
||||||
|
|
||||||
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
|
<OwnerNameDisplay ownerObject={event.job} />
|
||||||
</Space>
|
|
||||||
) : (
|
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||||
<div
|
(event.job && event.job.v_make_desc) || ""
|
||||||
style={{
|
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
||||||
}}
|
})`}
|
||||||
>
|
|
||||||
<strong>{`${event.title || ""}`}</strong>
|
{event.job && event.job.alt_transport && <div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>}
|
||||||
</div>
|
</Space>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>{`${event.title || ""}`}</strong>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[event, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(vis) => !event.vacation && setOpen(vis)}
|
onOpenChange={handleOpenChange}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
content={event.block ? blockContent : popoverContent}
|
content={event.block ? blockContent : popoverContent}
|
||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -347,4 +369,4 @@ export function ScheduleEventComponent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(React.memo(ScheduleEventComponent));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
@@ -10,64 +10,70 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
|||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import ScheduleEventComponent from "./schedule-event.component";
|
import ScheduleEventComponent from "./schedule-event.component";
|
||||||
|
|
||||||
export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
|
function ScheduleEventContainer({ bodyshop, event, refetch }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
|
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
|
||||||
const [updateJob] = useMutation(UPDATE_JOB);
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
const handleCancel = async ({ id, lost_sale_reason }) => {
|
|
||||||
logImEXEvent("schedule_cancel_appt");
|
|
||||||
|
|
||||||
const cancelAppt = await cancelAppointment({
|
const handleCancel = useCallback(
|
||||||
variables: { appid: event.id }
|
async ({ id, lost_sale_reason }) => {
|
||||||
});
|
logImEXEvent("schedule_cancel_appt");
|
||||||
notification["success"]({
|
|
||||||
message: t("appointments.successes.canceled")
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!!cancelAppt.errors) {
|
const cancelAppt = await cancelAppointment({
|
||||||
notification["error"]({
|
variables: { appid: event.id }
|
||||||
message: t("appointments.errors.canceling", {
|
|
||||||
message: JSON.stringify(cancelAppt.errors)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.job) {
|
if (!cancelAppt.errors) {
|
||||||
const jobUpdate = await updateJob({
|
notification.success({
|
||||||
variables: {
|
message: t("appointments.successes.canceled")
|
||||||
jobId: event.job.id,
|
});
|
||||||
job: {
|
} else {
|
||||||
date_scheduled: null,
|
notification.error({
|
||||||
scheduled_in: null,
|
message: t("appointments.errors.canceling", {
|
||||||
scheduled_completion: null,
|
message: JSON.stringify(cancelAppt.errors)
|
||||||
lost_sale_reason,
|
|
||||||
date_lost_sale: new Date(),
|
|
||||||
status: bodyshop.md_ro_statuses.default_imported
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!jobUpdate.errors) {
|
|
||||||
dispatch(
|
|
||||||
insertAuditTrail({
|
|
||||||
jobid: event.job.id,
|
|
||||||
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
|
||||||
type: "appointmentcancel"
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!!jobUpdate.errors) {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("jobs.errors.updating", {
|
|
||||||
message: JSON.stringify(jobUpdate.errors)
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (refetch) refetch();
|
if (event.job) {
|
||||||
};
|
const jobUpdate = await updateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: event.job.id,
|
||||||
|
job: {
|
||||||
|
date_scheduled: null,
|
||||||
|
scheduled_in: null,
|
||||||
|
scheduled_completion: null,
|
||||||
|
lost_sale_reason,
|
||||||
|
date_lost_sale: new Date(),
|
||||||
|
status: bodyshop.md_ro_statuses.default_imported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!jobUpdate.errors) {
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: event.job.id,
|
||||||
|
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||||
|
type: "appointmentcancel"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: t("jobs.errors.updating", {
|
||||||
|
message: JSON.stringify(jobUpdate.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (refetch) refetch();
|
||||||
|
},
|
||||||
|
[cancelAppointment, event.id, event.job, updateJob, bodyshop.md_ro_statuses.default_imported, dispatch, t, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
return <ScheduleEventComponent event={event} refetch={refetch} handleCancel={handleCancel} />;
|
return <ScheduleEventComponent event={event} refetch={refetch} handleCancel={handleCancel} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default React.memo(ScheduleEventContainer);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { EditFilled, SaveFilled } from "@ant-design/icons";
|
import { EditFilled, SaveFilled } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Input, notification, Space } from "antd";
|
import { Button, Input, notification, Space } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState, useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -12,9 +12,6 @@ import DataLabel from "../data-label/data-label.component";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ScheduleEventNote({ event }) {
|
export function ScheduleEventNote({ event }) {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
@@ -23,9 +20,9 @@ export function ScheduleEventNote({ event }) {
|
|||||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const toggleEdit = async () => {
|
const toggleEdit = useCallback(async () => {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
//Await the update
|
// Await the update
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateAppointment({
|
const result = await updateAppointment({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -34,10 +31,10 @@ export function ScheduleEventNote({ event }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!!!result.errors) {
|
if (!result.errors) {
|
||||||
// notification["success"]({ message: t("appointments.successes.saved") });
|
// notification.success({ message: t("appointments.successes.saved") });
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification.error({
|
||||||
message: t("jobs.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
error: JSON.stringify(result.errors)
|
error: JSON.stringify(result.errors)
|
||||||
})
|
})
|
||||||
@@ -45,11 +42,15 @@ export function ScheduleEventNote({ event }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
}, [editing, note, updateAppointment, event.id, t]);
|
||||||
};
|
|
||||||
|
const handleNoteChange = useCallback((e) => {
|
||||||
|
setNote(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataLabel label={t("appointments.fields.note")}>
|
<DataLabel label={t("appointments.fields.note")}>
|
||||||
@@ -57,7 +58,7 @@ export function ScheduleEventNote({ event }) {
|
|||||||
{!editing ? (
|
{!editing ? (
|
||||||
event.note || ""
|
event.note || ""
|
||||||
) : (
|
) : (
|
||||||
<Input.TextArea rows={3} value={note} onChange={(e) => setNote(e.target.value)} style={{ maxWidth: "8vw" }} />
|
<Input.TextArea rows={3} value={note} onChange={handleNoteChange} style={{ maxWidth: "8vw" }} />
|
||||||
)}
|
)}
|
||||||
<Button onClick={toggleEdit} loading={loading}>
|
<Button onClick={toggleEdit} loading={loading}>
|
||||||
{editing ? <SaveFilled /> : <EditFilled />}
|
{editing ? <SaveFilled /> : <EditFilled />}
|
||||||
@@ -67,4 +68,4 @@ export function ScheduleEventNote({ event }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventNote);
|
export default connect(mapStateToProps)(React.memo(ScheduleEventNote));
|
||||||
|
|||||||
Reference in New Issue
Block a user