Compare commits
7 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbd52091d8 | ||
|
|
873eb65e75 | ||
|
|
4b6e140e3e | ||
|
|
8f118937f3 | ||
|
|
cd0a08a7be | ||
|
|
b0ea516fd6 | ||
|
|
10ba19f0d2 |
8
client/package-lock.json
generated
8
client/package-lock.json
generated
@@ -47,7 +47,7 @@
|
|||||||
"query-string": "^9.1.0",
|
"query-string": "^9.1.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.13.2",
|
"react-big-calendar": "^1.14.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^7.2.0",
|
"react-cookie": "^7.2.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -14671,9 +14671,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-big-calendar": {
|
"node_modules/react-big-calendar": {
|
||||||
"version": "1.13.2",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.14.1.tgz",
|
||||||
"integrity": "sha512-yzeVRM1I+JloeJXytrZx2lJWKUfLAi5bsgGuBjh3aFSHZrdFcGnfA7LE6pBacdyOG+NGP+332m2MziszkmQWcw==",
|
"integrity": "sha512-6Le0kV/4yiV/mlqv5YYBBS+FaBeYBPNGjcYitLoVdPCiXsc0xzSHyX8+2FRqX9AM16XZYIjjomouK3wcnq6+XQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.7",
|
"@babel/runtime": "^7.20.7",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
"query-string": "^9.1.0",
|
"query-string": "^9.1.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.13.2",
|
"react-big-calendar": "^1.14.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^7.2.0",
|
"react-cookie": "^7.2.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { AlertFilled } from "@ant-design/icons";
|
|||||||
import { Button, Divider, Dropdown, Form, Input, notification, Popover, Select, Space } from "antd";
|
import { Button, Divider, Dropdown, Form, Input, notification, Popover, Select, Space } from "antd";
|
||||||
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 React, { useCallback, useMemo, useState } from "react";
|
||||||
import React, { useState } 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, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
@@ -27,6 +26,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 +44,319 @@ 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 [searchParams] = useSearchParams();
|
||||||
|
|
||||||
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(() => {
|
||||||
|
const params = new URLSearchParams(searchParams);
|
||||||
|
params.set("selected", event.job?.id);
|
||||||
|
navigate({ search: `?${params.toString()}` });
|
||||||
|
}, [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
|
console.log("hit");
|
||||||
wrap
|
return (
|
||||||
size="small"
|
<div style={{ maxWidth: "40vw" }}>
|
||||||
style={{
|
{!event.isintake ? (
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
<Space>
|
||||||
}}
|
<strong>{event.title}</strong>
|
||||||
>
|
<ScheduleEventColor event={event} />
|
||||||
{event.note && <AlertFilled className="production-alert" />}
|
</Space>
|
||||||
<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 +365,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));
|
||||||
|
|||||||
@@ -1,50 +1,36 @@
|
|||||||
import { Space } from "antd";
|
import { Space } from "antd";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectScheduleLoad } from "../../redux/application/application.selectors";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const ScheduleAtsSummary = React.memo(function ScheduleAtsSummary({ appointments }) {
|
||||||
//currentUser: selectCurrentUser
|
|
||||||
scheduleLoad: selectScheduleLoad
|
|
||||||
});
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ScheduleAtsSummary({ scheduleLoad, appointments }) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const atsSummary = useMemo(() => {
|
const atsSummary = useMemo(() => {
|
||||||
let atsSummary = {};
|
|
||||||
if (!appointments || appointments.length === 0) {
|
if (!appointments || appointments.length === 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
const summary = {};
|
||||||
appointments
|
appointments
|
||||||
.filter((a) => a.isintake)
|
.filter((a) => a.isintake && a.job?.alt_transport)
|
||||||
.forEach((a) => {
|
.forEach((a) => {
|
||||||
if (!a.job.alt_transport) return;
|
const key = a.job.alt_transport;
|
||||||
if (!atsSummary[a.job.alt_transport]) {
|
summary[key] = (summary[key] || 0) + 1;
|
||||||
atsSummary[a.job.alt_transport] = 1;
|
|
||||||
} else {
|
|
||||||
atsSummary[a.job.alt_transport] = atsSummary[a.job.alt_transport] + 1;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return atsSummary;
|
return summary;
|
||||||
}, [appointments]);
|
}, [appointments]);
|
||||||
|
|
||||||
if (Object.keys(atsSummary).length > 0)
|
if (Object.keys(atsSummary).length > 0) {
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{t("schedule.labels.atssummary")}
|
{t("schedule.labels.atssummary")}
|
||||||
{Object.keys(atsSummary).map((key) => (
|
{Object.entries(atsSummary).map(([key, value]) => (
|
||||||
<span key={key}>{`${key}: ${atsSummary[key]}`}</span>
|
<span key={key}>{`${key}: ${value}`}</span>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleAtsSummary);
|
export default ScheduleAtsSummary;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Dropdown, notification } from "antd";
|
import { Dropdown, notification } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import React from "react";
|
import React, { 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 { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -13,57 +13,61 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const ScheduleBlockDay = React.memo(function ScheduleBlockDay({ date, children, refetch, bodyshop, alreadyBlocked }) {
|
||||||
|
|
||||||
export function ScheduleBlockDay({ date, children, refetch, bodyshop, alreadyBlocked }) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [insertBlock] = useMutation(INSERT_APPOINTMENT_BLOCK);
|
const [insertBlock] = useMutation(INSERT_APPOINTMENT_BLOCK);
|
||||||
|
|
||||||
const handleMenu = async (e) => {
|
const handleMenu = useCallback(
|
||||||
e.domEvent.stopPropagation();
|
async (e) => {
|
||||||
|
e.domEvent.stopPropagation();
|
||||||
|
|
||||||
if (e.key === "block") {
|
if (e.key === "block") {
|
||||||
const blockAppt = {
|
const blockAppt = {
|
||||||
title: t("appointments.labels.blocked"),
|
title: t("appointments.labels.blocked"),
|
||||||
block: true,
|
block: true,
|
||||||
isintake: false,
|
isintake: false,
|
||||||
bodyshopid: bodyshop.id,
|
bodyshopid: bodyshop.id,
|
||||||
start: dayjs(date).startOf("day"),
|
start: dayjs(date).startOf("day"),
|
||||||
end: dayjs(date).endOf("day")
|
end: dayjs(date).endOf("day")
|
||||||
};
|
};
|
||||||
logImEXEvent("dashboard_change_layout");
|
logImEXEvent("dashboard_change_layout");
|
||||||
|
|
||||||
const result = await insertBlock({
|
const result = await insertBlock({
|
||||||
variables: { app: [blockAppt] }
|
variables: { app: [blockAppt] }
|
||||||
});
|
|
||||||
|
|
||||||
if (!!result.errors) {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("appointments.errors.blocking", {
|
|
||||||
message: JSON.stringify(result.errors)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (!!refetch) refetch();
|
if (result.errors) {
|
||||||
}
|
notification.error({
|
||||||
};
|
message: t("appointments.errors.blocking", {
|
||||||
|
message: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const menu = {
|
if (refetch) refetch();
|
||||||
items: [
|
|
||||||
{
|
|
||||||
key: "block",
|
|
||||||
label: t("appointments.actions.block")
|
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
onClick: handleMenu
|
[t, bodyshop.id, date, insertBlock, refetch]
|
||||||
};
|
);
|
||||||
|
|
||||||
|
const menu = useMemo(
|
||||||
|
() => ({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: "block",
|
||||||
|
label: t("appointments.actions.block")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onClick: handleMenu
|
||||||
|
}),
|
||||||
|
[t, handleMenu]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown menu={menu} disabled={alreadyBlocked} trigger={["contextMenu"]}>
|
<Dropdown menu={menu} disabled={alreadyBlocked} trigger={["contextMenu"]}>
|
||||||
{children}
|
{children}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleBlockDay);
|
export default connect(mapStateToProps)(ScheduleBlockDay);
|
||||||
|
|||||||
@@ -10,55 +10,48 @@ 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 ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
const ScheduleCalendarHeaderGraph = React.memo(function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
|
||||||
const { ssbuckets } = bodyshop;
|
const { ssbuckets } = bodyshop;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const data = useMemo(() => {
|
|
||||||
return (
|
|
||||||
(loadData &&
|
|
||||||
loadData.expectedLoad &&
|
|
||||||
Object.keys(loadData.expectedLoad).map((key) => {
|
|
||||||
const metadataBucket = ssbuckets.filter((b) => b.id === key)[0];
|
|
||||||
|
|
||||||
return {
|
const data = useMemo(() => {
|
||||||
bucket: loadData.expectedLoad[key].label,
|
if (!loadData || !loadData.expectedLoad || !ssbuckets) return [];
|
||||||
current: loadData.expectedLoad[key].count,
|
|
||||||
target: metadataBucket && metadataBucket.target
|
return Object.keys(loadData.expectedLoad).map((key) => {
|
||||||
};
|
const metadataBucket = ssbuckets.find((b) => b.id === key);
|
||||||
})) ||
|
|
||||||
[]
|
return {
|
||||||
);
|
bucket: loadData.expectedLoad[key].label,
|
||||||
|
current: loadData.expectedLoad[key].count,
|
||||||
|
target: metadataBucket?.target || 0
|
||||||
|
};
|
||||||
|
});
|
||||||
}, [loadData, ssbuckets]);
|
}, [loadData, ssbuckets]);
|
||||||
|
|
||||||
const popContent = (
|
const popContent = useMemo(
|
||||||
<div>
|
() => (
|
||||||
<Space>
|
<div>
|
||||||
{t("appointments.labels.expectedprodhrs")}
|
<Space>
|
||||||
<strong>{loadData?.expectedHours?.toFixed(1)}</strong>
|
{t("appointments.labels.expectedprodhrs")}
|
||||||
{t("appointments.labels.expectedjobs")}
|
<strong>{loadData?.expectedHours?.toFixed(1) || 0}</strong>
|
||||||
<strong>{loadData?.expectedJobCount}</strong>
|
{t("appointments.labels.expectedjobs")}
|
||||||
</Space>
|
<strong>{loadData?.expectedJobCount || 0}</strong>
|
||||||
<RadarChart
|
</Space>
|
||||||
// cx={300}
|
<RadarChart width={300} height={250} data={data}>
|
||||||
// cy={250}
|
<PolarGrid />
|
||||||
// outerRadius={150}
|
<PolarAngleAxis dataKey="bucket" />
|
||||||
width={800}
|
<PolarRadiusAxis angle={90} />
|
||||||
height={600}
|
<Radar name="Ideal Load" dataKey="target" stroke="darkgreen" fill="white" fillOpacity={0} />
|
||||||
data={data}
|
<Radar name="EOD Load" dataKey="current" stroke="dodgerblue" fill="dodgerblue" fillOpacity={0.6} />
|
||||||
>
|
<Tooltip />
|
||||||
<PolarGrid />
|
<Legend />
|
||||||
<PolarAngleAxis dataKey="bucket" />
|
</RadarChart>
|
||||||
<PolarRadiusAxis angle={90} />
|
</div>
|
||||||
<Radar name="Ideal Load" dataKey="target" stroke="darkgreen" fill="white" fillOpacity={0} />
|
),
|
||||||
<Radar name="EOD Load" dataKey="current" stroke="dodgerblue" fill="dodgerblue" fillOpacity={0.6} />
|
[t, loadData, data]
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
</RadarChart>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -66,6 +59,6 @@ export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
|
|||||||
<RadarChartOutlined />
|
<RadarChartOutlined />
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderGraph);
|
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderGraph);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import React, { useCallback, useMemo } from "react";
|
||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
import { Popover, Space } from "antd";
|
import { Popover, Space } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
import React, { useMemo } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MdFileDownload, MdFileUpload } from "react-icons/md";
|
import { MdFileDownload, MdFileUpload } from "react-icons/md";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -24,115 +23,114 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
calculating: selectScheduleLoadCalculating
|
calculating: selectScheduleLoadCalculating
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
export function ScheduleCalendarHeaderComponent({
|
export const ScheduleCalendarHeaderComponent = React.memo(function ScheduleCalendarHeaderComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
label,
|
label,
|
||||||
refetch,
|
refetch,
|
||||||
date,
|
date,
|
||||||
load,
|
load,
|
||||||
calculating,
|
calculating,
|
||||||
events,
|
events
|
||||||
...otherProps
|
|
||||||
}) {
|
}) {
|
||||||
|
const dayjsDate = useMemo(() => dayjs(date), [date]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const ATSToday = useMemo(() => {
|
const ATSToday = useMemo(() => {
|
||||||
if (!events) return [];
|
if (!events) return [];
|
||||||
return _.groupBy(
|
const filteredEvents = events.filter((e) => !e.vacation && e.isintake && dayjsDate.isSame(dayjs(e.start), "day"));
|
||||||
events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")),
|
return _.groupBy(filteredEvents, "job.alt_transport");
|
||||||
"job.alt_transport"
|
}, [events, dayjsDate]);
|
||||||
);
|
|
||||||
}, [events, date]);
|
|
||||||
|
|
||||||
const isDayBlocked = useMemo(() => {
|
const isDayBlocked = useMemo(() => {
|
||||||
if (!events) return [];
|
if (!events) return [];
|
||||||
return events && events.filter((e) => dayjs(date).isSame(dayjs(e.start), "day") && e.block);
|
return events.filter((e) => dayjsDate.isSame(dayjs(e.start), "day") && e.block);
|
||||||
}, [events, date]);
|
}, [events, dayjsDate]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const dateString = dayjsDate.format("YYYY-MM-DD");
|
||||||
const loadData = load[date.toISOString().substr(0, 10)];
|
const loadData = load[dateString];
|
||||||
|
|
||||||
const jobsOutPopup = () => (
|
const jobsOutPopup = useCallback(
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
() => (
|
||||||
<table>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<tbody>
|
<table>
|
||||||
{loadData && loadData.allJobsOut ? (
|
<tbody>
|
||||||
loadData.allJobsOut.map((j) => (
|
{loadData && loadData.allJobsOut && loadData.allJobsOut.length > 0 ? (
|
||||||
<tr key={j.id}>
|
loadData.allJobsOut.map((j) => (
|
||||||
<td style={{ padding: "2.5px" }}>
|
<tr key={j.id}>
|
||||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> (
|
<td style={{ padding: "2.5px" }}>
|
||||||
{j.status})
|
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> ({j.status})
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: "2.5px" }}>
|
<td style={{ padding: "2.5px" }}>
|
||||||
<OwnerNameDisplay ownerObject={j} />
|
<OwnerNameDisplay ownerObject={j} />
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: "2.5px" }}>
|
<td style={{ padding: "2.5px" }}>
|
||||||
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
|
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
|
||||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
||||||
}/${(
|
}/${(
|
||||||
j.labhrs.aggregate?.sum?.mod_lb_hrs +
|
(j.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (j.larhrs?.aggregate?.sum?.mod_lb_hrs || 0)
|
||||||
j.larhrs.aggregate?.sum?.mod_lb_hrs
|
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
</td>
|
||||||
</td>
|
<td style={{ padding: "2.5px" }}>
|
||||||
<td style={{ padding: "2.5px" }}>
|
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
||||||
<DateTimeFormatter>
|
</td>
|
||||||
{j.scheduled_completion}
|
</tr>
|
||||||
</DateTimeFormatter>
|
))
|
||||||
</td>
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td style={{ padding: "2.5px" }}>{t("appointments.labels.nocompletingjobs")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
)}
|
||||||
) : (
|
</tbody>
|
||||||
<tr>
|
</table>
|
||||||
<td style={{ padding: "2.5px" }}>
|
</div>
|
||||||
{t("appointments.labels.nocompletingjobs")}
|
),
|
||||||
</td>
|
[loadData, t]
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const jobsInPopup = () => (
|
const jobsInPopup = useCallback(
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
() => (
|
||||||
<table>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<tbody>
|
<table>
|
||||||
{loadData && loadData.allJobsIn ? (
|
<tbody>
|
||||||
loadData.allJobsIn.map((j) => (
|
{loadData && loadData.allJobsIn && loadData.allJobsIn.length > 0 ? (
|
||||||
<tr key={j.id}>
|
loadData.allJobsIn.map((j) => (
|
||||||
<td style={{ padding: "2.5px" }}>
|
<tr key={j.id}>
|
||||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
<td style={{ padding: "2.5px" }}>
|
||||||
</td>
|
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||||
<td style={{ padding: "2.5px" }}>
|
</td>
|
||||||
<OwnerNameDisplay ownerObject={j} />
|
<td style={{ padding: "2.5px" }}>
|
||||||
</td>
|
<OwnerNameDisplay ownerObject={j} />
|
||||||
<td style={{ padding: "2.5px" }}>
|
</td>
|
||||||
{`(${j.labhrs?.aggregate?.sum.mod_lb_hrs?.toFixed(1) || 0}/${
|
<td style={{ padding: "2.5px" }}>
|
||||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
{`(${j.labhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0}/${
|
||||||
}/${(
|
j.larhrs?.aggregate?.sum?.mod_lb_hrs?.toFixed(1) || 0
|
||||||
j.labhrs?.aggregate?.sum?.mod_lb_hrs +
|
}/${(
|
||||||
j.larhrs?.aggregate?.sum?.mod_lb_hrs
|
(j.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (j.larhrs?.aggregate?.sum?.mod_lb_hrs || 0)
|
||||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: "2.5px" }}>
|
<td style={{ padding: "2.5px" }}>
|
||||||
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
|
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td style={{ padding: "2.5px" }}>{t("appointments.labels.noarrivingjobs")}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
)}
|
||||||
) : (
|
</tbody>
|
||||||
<tr>
|
</table>
|
||||||
<td style={{ padding: "2.5px" }}>
|
</div>
|
||||||
{t("appointments.labels.noarrivingjobs")}
|
),
|
||||||
</td>
|
[loadData, t]
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const LoadComponent = loadData ? (
|
const LoadComponent = useMemo(() => {
|
||||||
<div>
|
if (!loadData) return null;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
<Popover
|
<Popover
|
||||||
placement={"bottom"}
|
placement={"bottom"}
|
||||||
@@ -141,12 +139,8 @@ export function ScheduleCalendarHeaderComponent({
|
|||||||
title={t("appointments.labels.arrivingjobs")}
|
title={t("appointments.labels.arrivingjobs")}
|
||||||
>
|
>
|
||||||
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
||||||
{(loadData.allHoursInBody || 0) &&
|
{(loadData.allHoursInBody || 0).toFixed(1)}/{(loadData.allHoursInRefinish || 0).toFixed(1)}/
|
||||||
loadData.allHoursInBody.toFixed(1)}
|
{(loadData.allHoursIn || 0).toFixed(1)}
|
||||||
/
|
|
||||||
{(loadData.allHoursInRefinish || 0) &&
|
|
||||||
loadData.allHoursInRefinish.toFixed(1)}
|
|
||||||
/{(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}
|
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover
|
<Popover
|
||||||
placement={"bottom"}
|
placement={"bottom"}
|
||||||
@@ -155,57 +149,31 @@ export function ScheduleCalendarHeaderComponent({
|
|||||||
title={t("appointments.labels.completingjobs")}
|
title={t("appointments.labels.completingjobs")}
|
||||||
>
|
>
|
||||||
<Icon component={MdFileUpload} style={{ color: "red" }} />
|
<Icon component={MdFileUpload} style={{ color: "red" }} />
|
||||||
{(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(1)}
|
{(loadData.allHoursOut || 0).toFixed(1)}
|
||||||
</Popover>
|
</Popover>
|
||||||
<ScheduleCalendarHeaderGraph loadData={loadData} />
|
<ScheduleCalendarHeaderGraph loadData={loadData} />
|
||||||
</Space>
|
</Space>
|
||||||
|
<div>
|
||||||
<div>
|
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>
|
||||||
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>
|
{Object.keys(ATSToday).map((key, idx) => (
|
||||||
{Object.keys(ATSToday).map((key, idx) => (
|
<li key={idx}>{`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}</li>
|
||||||
<li key={idx}>{`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}</li>
|
))}
|
||||||
))}
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
) : null;
|
}, [loadData, jobsInPopup, jobsOutPopup, t, ATSToday]);
|
||||||
|
|
||||||
const isShopOpen = (date) => {
|
|
||||||
let day;
|
|
||||||
switch (dayjs(date).day()) {
|
|
||||||
case 0:
|
|
||||||
day = "sunday";
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
day = "monday";
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
day = "tuesday";
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
day = "wednesday";
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
day = "thursday";
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
day = "friday";
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
day = "saturday";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
day = "sunday";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const isShopOpen = useCallback(() => {
|
||||||
|
const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
||||||
|
const day = days[dayjsDate.day()];
|
||||||
return bodyshop.workingdays[day];
|
return bodyshop.workingdays[day];
|
||||||
};
|
}, [bodyshop, dayjsDate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="imex-calendar-load">
|
<div className="imex-calendar-load">
|
||||||
<ScheduleBlockDay alreadyBlocked={isDayBlocked.length > 0} date={date} refetch={refetch}>
|
<ScheduleBlockDay alreadyBlocked={isDayBlocked.length > 0} date={date} refetch={refetch}>
|
||||||
<div style={{ color: isShopOpen(date) ? "" : "tomato" }}>
|
<div style={{ color: isShopOpen() ? "" : "tomato" }}>
|
||||||
{label}
|
{label}
|
||||||
{InstanceRenderMgr({
|
{InstanceRenderMgr({
|
||||||
imex: calculating ? <LoadingSkeleton /> : LoadComponent,
|
imex: calculating ? <LoadingSkeleton /> : LoadComponent,
|
||||||
@@ -216,6 +184,6 @@ export function ScheduleCalendarHeaderComponent({
|
|||||||
</ScheduleBlockDay>
|
</ScheduleBlockDay>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderComponent);
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
export function getRange(dateParam, viewParam) {
|
// Predefine range calculation functions for each view
|
||||||
let start, end;
|
const viewRanges = {
|
||||||
let date = dateParam || new Date();
|
day: (date) => ({
|
||||||
let view = viewParam || "week";
|
start: date.startOf("day"),
|
||||||
// if view is day: from dayjs(date).startOf('day') to dayjs(date).endOf('day');
|
end: date.endOf("day")
|
||||||
if (view === "day") {
|
}),
|
||||||
start = dayjs(date).startOf("day");
|
week: (date) => ({
|
||||||
end = dayjs(date).endOf("day");
|
start: date.startOf("week"),
|
||||||
}
|
end: date.endOf("week")
|
||||||
// if view is week: from dayjs(date).startOf('isoWeek') to dayjs(date).endOf('isoWeek');
|
}),
|
||||||
else if (view === "week") {
|
month: (date) => ({
|
||||||
start = dayjs(date).startOf("week");
|
// Adjusting for adjacent weeks
|
||||||
end = dayjs(date).endOf("week");
|
start: date.startOf("month").subtract(7, "day"),
|
||||||
}
|
end: date.endOf("month").add(7, "day")
|
||||||
//if view is month: from dayjs(date).startOf('month').subtract(7, 'day') to dayjs(date).endOf('month').add(7, 'day'); i do additional 7 days math because you can see adjacent weeks on month view (that is the way how i generate my recurrent events for the Big Calendar, but if you need only start-end of month - just remove that math);
|
}),
|
||||||
else if (view === "month") {
|
agenda: (date) => ({
|
||||||
start = dayjs(date).startOf("month").subtract(7, "day");
|
start: date.startOf("day"),
|
||||||
end = dayjs(date).endOf("month").add(7, "day");
|
end: date.endOf("day").add(1, "month")
|
||||||
}
|
})
|
||||||
// if view is agenda: from dayjs(date).startOf('day') to dayjs(date).endOf('day').add(1, 'month');
|
};
|
||||||
else if (view === "agenda") {
|
|
||||||
start = dayjs(date).startOf("day");
|
|
||||||
end = dayjs(date).endOf("day").add(1, "month");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { start, end };
|
export function getRange(dateParam = new Date(), viewParam = "week") {
|
||||||
|
const date = dayjs(dateParam);
|
||||||
|
const view = viewRanges[viewParam] ? viewParam : "week";
|
||||||
|
return viewRanges[view](date);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { Calendar, dayjsLocalizer } from "react-big-calendar";
|
import { Calendar, dayjsLocalizer } from "react-big-calendar";
|
||||||
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";
|
||||||
@@ -19,9 +19,10 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
problemJobs: selectProblemJobs
|
problemJobs: selectProblemJobs
|
||||||
});
|
});
|
||||||
|
|
||||||
const localizer = dayjsLocalizer(dayjs);
|
const localizer = dayjsLocalizer(dayjs);
|
||||||
|
|
||||||
export function ScheduleCalendarWrapperComponent({
|
export const ScheduleCalendarWrapperComponent = React.memo(function ScheduleCalendarWrapperComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
problemJobs,
|
problemJobs,
|
||||||
data,
|
data,
|
||||||
@@ -31,23 +32,79 @@ export function ScheduleCalendarWrapperComponent({
|
|||||||
date,
|
date,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) {
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const location = useLocation();
|
||||||
const history = useNavigate();
|
const search = useMemo(() => queryString.parse(location.search), [location.search]);
|
||||||
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleEventPropStyles = (event, start, end, isSelected) => {
|
|
||||||
return {
|
|
||||||
...(event.color && !((search.view || defaultView) === "agenda")
|
|
||||||
? {
|
|
||||||
style: {
|
|
||||||
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
className: `${event.arrived ? "imex-event-arrived" : ""} ${event.block ? "imex-event-block" : ""}`
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedDate = new Date(date || dayjs(search.date) || Date.now());
|
const selectedDate = useMemo(() => {
|
||||||
|
return new Date(date || dayjs(search.date).toDate() || Date.now());
|
||||||
|
}, [date, search.date]);
|
||||||
|
|
||||||
|
const minTime = useMemo(() => {
|
||||||
|
return bodyshop.schedule_start_time ? new Date(bodyshop.schedule_start_time) : new Date("2020-01-01T06:00:00");
|
||||||
|
}, [bodyshop.schedule_start_time]);
|
||||||
|
|
||||||
|
const maxTime = useMemo(() => {
|
||||||
|
return bodyshop.schedule_end_time ? new Date(bodyshop.schedule_end_time) : new Date("2020-01-01T20:00:00");
|
||||||
|
}, [bodyshop.schedule_end_time]);
|
||||||
|
|
||||||
|
const handleEventPropStyles = useCallback(
|
||||||
|
(event, start, end, isSelected) => {
|
||||||
|
return {
|
||||||
|
...(event.color && !((search.view || defaultView) === "agenda")
|
||||||
|
? {
|
||||||
|
style: {
|
||||||
|
backgroundColor: event.color && event.color.hex ? event.color.hex : event.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
className: `${event.arrived ? "imex-event-arrived" : ""} ${event.block ? "imex-event-block" : ""}`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[search.view, defaultView]
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventComponent = useCallback(
|
||||||
|
(e) => <Event bodyshop={bodyshop} event={e.event} refetch={refetch} />,
|
||||||
|
[bodyshop, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerComponent = useCallback(
|
||||||
|
(p) => <HeaderComponent {...p} events={data} refetch={refetch} />,
|
||||||
|
[data, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarComponents = useMemo(
|
||||||
|
() => ({
|
||||||
|
event: eventComponent,
|
||||||
|
header: headerComponent
|
||||||
|
}),
|
||||||
|
[eventComponent, headerComponent]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNavigate = useCallback(
|
||||||
|
(date, view, action) => {
|
||||||
|
const newSearch = { ...search, date: date.toISOString().substr(0, 10) };
|
||||||
|
navigate({ search: queryString.stringify(newSearch) });
|
||||||
|
},
|
||||||
|
[search, navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onView = useCallback(
|
||||||
|
(view) => {
|
||||||
|
const newSearch = { ...search, view };
|
||||||
|
navigate({ search: queryString.stringify(newSearch) });
|
||||||
|
},
|
||||||
|
[search, navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRangeChange = useCallback(
|
||||||
|
(range) => {
|
||||||
|
if (setDateRangeCallback) setDateRangeCallback(range);
|
||||||
|
},
|
||||||
|
[setDateRangeCallback]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -109,32 +166,20 @@ export function ScheduleCalendarWrapperComponent({
|
|||||||
events={data}
|
events={data}
|
||||||
defaultView={search.view || defaultView || "week"}
|
defaultView={search.view || defaultView || "week"}
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
onNavigate={(date, view, action) => {
|
onNavigate={onNavigate}
|
||||||
search.date = date.toISOString().substr(0, 10);
|
onRangeChange={onRangeChange}
|
||||||
history({ search: queryString.stringify(search) });
|
onView={onView}
|
||||||
}}
|
|
||||||
onRangeChange={(start, end) => {
|
|
||||||
if (setDateRangeCallback) setDateRangeCallback({ start, end });
|
|
||||||
}}
|
|
||||||
onView={(view) => {
|
|
||||||
search.view = view;
|
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
}}
|
|
||||||
step={15}
|
step={15}
|
||||||
// timeslots={1}
|
|
||||||
showMultiDayTimes
|
showMultiDayTimes
|
||||||
localizer={localizer}
|
localizer={localizer}
|
||||||
min={bodyshop.schedule_start_time ? new Date(bodyshop.schedule_start_time) : new Date("2020-01-01T06:00:00")}
|
min={minTime}
|
||||||
max={bodyshop.schedule_end_time ? new Date(bodyshop.schedule_end_time) : new Date("2020-01-01T20:00:00")}
|
max={maxTime}
|
||||||
eventPropGetter={handleEventPropStyles}
|
eventPropGetter={handleEventPropStyles}
|
||||||
components={{
|
components={calendarComponents}
|
||||||
event: (e) => Event({ bodyshop: bodyshop, event: e.event, refetch: refetch }),
|
|
||||||
header: (p) => <HeaderComponent {...p} events={data} refetch={refetch} />
|
|
||||||
}}
|
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(ScheduleCalendarWrapperComponent);
|
export default connect(mapStateToProps, null)(ScheduleCalendarWrapperComponent);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Checkbox, Col, Row, Select, Space } from "antd";
|
import { Button, Card, Checkbox, Col, Row, Select, Space } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { t } from "i18next";
|
import React, { Profiler, useCallback, useMemo } from "react";
|
||||||
import React, { useMemo } from "react";
|
import { useTranslation } from "react-i18next";
|
||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
import ScheduleAtsSummary from "../schedule-ats-summary/schedule-ats-summary.component";
|
import ScheduleAtsSummary from "../schedule-ats-summary/schedule-ats-summary.component";
|
||||||
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
|
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
|
||||||
@@ -18,19 +18,17 @@ import _ from "lodash";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarComponent);
|
|
||||||
|
|
||||||
export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
const ScheduleCalendarComponent = React.memo(function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [filter, setFilter] = useLocalStorage("filter_events", {
|
const [filter, setFilter] = useLocalStorage("filter_events", {
|
||||||
intake: true,
|
intake: true,
|
||||||
manual: true,
|
manual: true,
|
||||||
employeevacation: true,
|
employeevacation: true,
|
||||||
ins_co_nm: null
|
ins_co_nm: null
|
||||||
});
|
});
|
||||||
const [estimatorsFilter, setEstimatiorsFilter] = useLocalStorage("estimators", []);
|
const [estimatorsFilter, setEstimatorsFilter] = useLocalStorage("estimators", []);
|
||||||
|
|
||||||
const estimators = useMemo(() => {
|
const estimators = useMemo(() => {
|
||||||
return _.uniq([
|
return _.uniq([
|
||||||
@@ -48,7 +46,7 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
|||||||
d.__typename === "appointments"
|
d.__typename === "appointments"
|
||||||
? estimatorsFilter.length === 0
|
? estimatorsFilter.length === 0
|
||||||
? true
|
? true
|
||||||
: !!estimatorsFilter.find((e) => e === `${d.job?.est_ct_fn || ""} ${d.job?.est_ct_ln || ""}`.trim())
|
: estimatorsFilter.includes(`${d.job?.est_ct_fn || ""} ${d.job?.est_ct_ln || ""}`.trim())
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -62,7 +60,85 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
|||||||
});
|
});
|
||||||
}, [data, filter, estimatorsFilter]);
|
}, [data, filter, estimatorsFilter]);
|
||||||
|
|
||||||
|
const estimatorsOptions = useMemo(() => {
|
||||||
|
return estimators.map((e) => ({
|
||||||
|
label: e,
|
||||||
|
value: e
|
||||||
|
}));
|
||||||
|
}, [estimators]);
|
||||||
|
|
||||||
|
const insCoNmOptions = useMemo(() => {
|
||||||
|
return bodyshop.md_ins_cos.map((i) => ({
|
||||||
|
label: i.name,
|
||||||
|
value: i.name
|
||||||
|
}));
|
||||||
|
}, [bodyshop.md_ins_cos]);
|
||||||
|
|
||||||
|
const handleEstimatorsFilterChange = useCallback(
|
||||||
|
(e) => {
|
||||||
|
setEstimatorsFilter(e);
|
||||||
|
},
|
||||||
|
[setEstimatorsFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEstimatorsFilterClear = useCallback(() => {
|
||||||
|
setEstimatorsFilter([]);
|
||||||
|
}, [setEstimatorsFilter]);
|
||||||
|
|
||||||
|
const handleInsCoNmFilterChange = useCallback(
|
||||||
|
(e) => {
|
||||||
|
setFilter((prevFilter) => ({ ...prevFilter, ins_co_nm: e }));
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInsCoNmFilterClear = useCallback(() => {
|
||||||
|
setFilter((prevFilter) => ({ ...prevFilter, ins_co_nm: [] }));
|
||||||
|
}, [setFilter]);
|
||||||
|
|
||||||
|
const handleIntakeFilterChange = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
setFilter((prevFilter) => ({ ...prevFilter, intake: checked }));
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleManualFilterChange = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
setFilter((prevFilter) => ({ ...prevFilter, manual: checked }));
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEmployeeVacationFilterChange = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
setFilter((prevFilter) => ({ ...prevFilter, employeevacation: checked }));
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRefetch = useCallback(() => {
|
||||||
|
refetch();
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// TODO Remove when done
|
||||||
|
// <Profiler
|
||||||
|
// id="cal"
|
||||||
|
// onRender={(id, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
||||||
|
// console.dir({
|
||||||
|
// id,
|
||||||
|
// phase,
|
||||||
|
// actualDuration,
|
||||||
|
// baseDuration,
|
||||||
|
// startTime,
|
||||||
|
// commitTime
|
||||||
|
// });
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<ScheduleModal />
|
<ScheduleModal />
|
||||||
|
|
||||||
@@ -76,65 +152,35 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
|||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder={t("schedule.labels.estimators")}
|
placeholder={t("schedule.labels.estimators")}
|
||||||
allowClear
|
allowClear
|
||||||
onClear={() => setEstimatiorsFilter([])}
|
onClear={handleEstimatorsFilterClear}
|
||||||
value={[...estimatorsFilter]}
|
value={estimatorsFilter}
|
||||||
onChange={(e) => {
|
onChange={handleEstimatorsFilterChange}
|
||||||
setEstimatiorsFilter(e);
|
options={estimatorsOptions}
|
||||||
}}
|
|
||||||
options={estimators.map((e) => ({
|
|
||||||
label: e,
|
|
||||||
value: e
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
style={{ minWidth: "15rem" }}
|
style={{ minWidth: "15rem" }}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder={t("schedule.labels.ins_co_nm_filter")}
|
placeholder={t("schedule.labels.ins_co_nm_filter")}
|
||||||
allowClear
|
allowClear
|
||||||
onClear={() => setFilter({ ...filter, ins_co_nm: [] })}
|
onClear={handleInsCoNmFilterClear}
|
||||||
value={filter?.ins_co_nm ? filter.ins_co_nm : []}
|
value={filter.ins_co_nm || []}
|
||||||
onChange={(e) => {
|
onChange={handleInsCoNmFilterChange}
|
||||||
setFilter({ ...filter, ins_co_nm: e });
|
options={insCoNmOptions}
|
||||||
}}
|
|
||||||
options={bodyshop.md_ins_cos.map((i) => ({
|
|
||||||
label: i.name,
|
|
||||||
value: i.name
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox checked={filter.intake} onChange={handleIntakeFilterChange}>
|
||||||
checked={filter?.intake}
|
|
||||||
onChange={(e) => {
|
|
||||||
setFilter({ ...filter, intake: e.target.checked });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("schedule.labels.intake")}
|
{t("schedule.labels.intake")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox
|
<Checkbox checked={filter.manual} onChange={handleManualFilterChange}>
|
||||||
checked={filter?.manual}
|
|
||||||
onChange={(e) => {
|
|
||||||
setFilter({ ...filter, manual: e.target.checked });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("schedule.labels.manual")}
|
{t("schedule.labels.manual")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox
|
<Checkbox checked={filter.employeevacation} onChange={handleEmployeeVacationFilterChange}>
|
||||||
checked={filter?.employeevacation}
|
|
||||||
onChange={(e) => {
|
|
||||||
setFilter({ ...filter, employeevacation: e.target.checked });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("schedule.labels.employeevacation")}
|
{t("schedule.labels.employeevacation")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<ScheduleVerifyIntegrity />
|
<ScheduleVerifyIntegrity />
|
||||||
<Button
|
<Button onClick={handleRefetch}>
|
||||||
onClick={() => {
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<ScheduleProductionList />
|
<ScheduleProductionList />
|
||||||
|
|
||||||
<ScheduleManualEvent />
|
<ScheduleManualEvent />
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -147,5 +193,9 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
// TODO Remove when done
|
||||||
|
// </Profiler>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ScheduleCalendarComponent);
|
||||||
|
|||||||
@@ -15,56 +15,65 @@ import dayjs from "../../utils/day";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
|
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ScheduleCalendarContainer({ calculateScheduleLoad }) {
|
const ScheduleCalendarContainer = React.memo(function ScheduleCalendarContainer({ calculateScheduleLoad }) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const location = useLocation();
|
||||||
|
const search = useMemo(() => queryString.parse(location.search), [location.search]);
|
||||||
|
|
||||||
const { date, view } = search;
|
const { date, view } = search;
|
||||||
const range = useMemo(() => getRange(date, view), [date, view]);
|
const range = useMemo(() => getRange(date, view), [date, view]);
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_APPOINTMENTS, {
|
const queryVariables = useMemo(
|
||||||
variables: {
|
() => ({
|
||||||
start: range.start.toDate(),
|
start: range.start.toDate(),
|
||||||
end: range.end.toDate(),
|
end: range.end.toDate(),
|
||||||
startd: range.start,
|
startd: range.start,
|
||||||
endd: range.end
|
endd: range.end
|
||||||
},
|
}),
|
||||||
skip: !!!range.start || !!!range.end,
|
[range]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_APPOINTMENTS, {
|
||||||
|
variables: queryVariables,
|
||||||
|
skip: !range.start || !range.end,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && range.end) calculateScheduleLoad(range.end);
|
if (data && range.end) {
|
||||||
}, [data, range, calculateScheduleLoad]);
|
calculateScheduleLoad(range.end);
|
||||||
|
}
|
||||||
|
}, [data, range.end, calculateScheduleLoad]);
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
const normalizedData = useMemo(() => {
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (!data) return [];
|
||||||
let normalizedData = [
|
return [
|
||||||
...data.appointments.map((e) => {
|
...data.appointments.map((e) => ({
|
||||||
//Required because Hasura returns a string instead of a date object.
|
|
||||||
return Object.assign({}, e, { start: new Date(e.start) }, { end: new Date(e.end) });
|
|
||||||
}),
|
|
||||||
...data.employee_vacation.map((e) => {
|
|
||||||
//Required because Hasura returns a string instead of a date object.
|
|
||||||
return {
|
|
||||||
...e,
|
...e,
|
||||||
title: `${
|
start: new Date(e.start),
|
||||||
(e.employee.first_name && e.employee.first_name.substr(0, 1)) || ""
|
end: new Date(e.end)
|
||||||
} ${e.employee.last_name || ""} OUT`,
|
})),
|
||||||
|
...data.employee_vacation.map((e) => ({
|
||||||
|
...e,
|
||||||
|
title: `${e.employee.first_name?.[0] || ""} ${e.employee.last_name || ""} OUT`,
|
||||||
color: "red",
|
color: "red",
|
||||||
start: dayjs(e.start).startOf("day").toDate(),
|
start: dayjs(e.start).startOf("day").toDate(),
|
||||||
end: dayjs(e.end).startOf("day").toDate(),
|
end: dayjs(e.end).startOf("day").toDate(),
|
||||||
allDay: true,
|
allDay: true,
|
||||||
vacation: true
|
vacation: true
|
||||||
};
|
}))
|
||||||
})
|
];
|
||||||
];
|
}, [data]);
|
||||||
|
|
||||||
return <ScheduleCalendarComponent refetch={refetch} data={data ? normalizedData : []} />;
|
if (loading) return <LoadingSpinner />;
|
||||||
}
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
|
return <ScheduleCalendarComponent refetch={refetch} data={normalizedData} />;
|
||||||
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarContainer);
|
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarContainer);
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ import React from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
|
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
|
||||||
|
|
||||||
export default function ScheduleDayViewComponent({ data, day }) {
|
const ScheduleDayViewComponent = React.memo(function ScheduleDayViewComponent({ data, day }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (data)
|
|
||||||
return <ScheduleCalendarWrapperComponent events={data} defaultView="day" view={"day"} views={["day"]} date={day} />;
|
if (data) {
|
||||||
else return <div>{t("appointments.labels.nodateselected")}</div>;
|
return <ScheduleCalendarWrapperComponent events={data} defaultView="day" view="day" views={["day"]} date={day} />;
|
||||||
}
|
} else {
|
||||||
|
return <div>{t("appointments.labels.nodateselected")}</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ScheduleDayViewComponent;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import ScheduleDayViewComponent from "./schedule-day-view.component";
|
import ScheduleDayViewComponent from "./schedule-day-view.component";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { QUERY_APPOINTMENT_BY_DATE } from "../../graphql/appointments.queries";
|
import { QUERY_APPOINTMENT_BY_DATE } from "../../graphql/appointments.queries";
|
||||||
@@ -6,45 +6,59 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
|||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function ScheduleDayViewContainer({ day }) {
|
const ScheduleDayViewContainer = React.memo(function ScheduleDayViewContainer({ day }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// Memoize dayjs computations
|
||||||
|
const dayjsDay = useMemo(() => dayjs(day), [day]);
|
||||||
|
|
||||||
|
// Memoize query variables
|
||||||
|
const queryVariables = useMemo(
|
||||||
|
() => ({
|
||||||
|
start: dayjsDay.startOf("day").toISOString(),
|
||||||
|
end: dayjsDay.endOf("day").toISOString(),
|
||||||
|
startd: dayjsDay.startOf("day").format("YYYY-MM-DD"),
|
||||||
|
endd: dayjsDay.add(1, "day").format("YYYY-MM-DD")
|
||||||
|
}),
|
||||||
|
[dayjsDay]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the useQuery hook
|
||||||
const { loading, error, data } = useQuery(QUERY_APPOINTMENT_BY_DATE, {
|
const { loading, error, data } = useQuery(QUERY_APPOINTMENT_BY_DATE, {
|
||||||
variables: {
|
variables: queryVariables,
|
||||||
start: dayjs(day).startOf("day"),
|
skip: !dayjsDay.isValid(),
|
||||||
end: dayjs(day).endOf("day"),
|
|
||||||
startd: dayjs(day).startOf("day").format("YYYY-MM-DD"),
|
|
||||||
endd: dayjs(day).add(1, "day").format("YYYY-MM-DD")
|
|
||||||
},
|
|
||||||
skip: !dayjs(day).isValid(),
|
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
const { t } = useTranslation();
|
|
||||||
|
// Memoize normalizedData
|
||||||
|
const normalizedData = useMemo(() => {
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
const appointments = data.appointments.map((e) => ({
|
||||||
|
...e,
|
||||||
|
start: new Date(e.start),
|
||||||
|
end: new Date(e.end)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const vacations = data.employee_vacation.map((e) => ({
|
||||||
|
...e,
|
||||||
|
title: `${e.employee.first_name?.[0] || ""} ${e.employee.last_name || ""} OUT`,
|
||||||
|
color: "red",
|
||||||
|
start: dayjs(e.start).startOf("day").toDate(),
|
||||||
|
end: dayjs(e.end).startOf("day").toDate(),
|
||||||
|
vacation: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...appointments, ...vacations];
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
// Handle conditional rendering
|
||||||
if (!day) return <div>{t("appointments.labels.nodateselected")}</div>;
|
if (!day) return <div>{t("appointments.labels.nodateselected")}</div>;
|
||||||
if (loading) return <LoadingSkeleton paragraph={{ rows: 4 }} />;
|
if (loading) return <LoadingSkeleton paragraph={{ rows: 4 }} />;
|
||||||
if (error) return <div>{error.message}</div>;
|
if (error) return <div>{error.message}</div>;
|
||||||
let normalizedData;
|
|
||||||
|
|
||||||
if (data) {
|
return <ScheduleDayViewComponent data={normalizedData} day={day} />;
|
||||||
normalizedData = [
|
});
|
||||||
...data.appointments.map((e) => {
|
|
||||||
//Required becuase Hasura returns a string instead of a date object.
|
|
||||||
return Object.assign({}, e, { start: new Date(e.start) }, { end: new Date(e.end) });
|
|
||||||
}),
|
|
||||||
...data.employee_vacation.map((e) => {
|
|
||||||
//Required becuase Hasura returns a string instead of a date object.
|
|
||||||
return {
|
|
||||||
...e,
|
|
||||||
title: `${
|
|
||||||
(e.employee.first_name && e.employee.first_name.substr(0, 1)) || ""
|
|
||||||
} ${e.employee.last_name || ""} OUT`,
|
|
||||||
color: "red",
|
|
||||||
start: dayjs(e.start).startOf("day").toDate(),
|
|
||||||
end: dayjs(e.end).startOf("day").toDate(),
|
|
||||||
vacation: true
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ScheduleDayViewComponent data={data ? normalizedData : []} day={day} />;
|
export default ScheduleDayViewContainer;
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,38 +1,43 @@
|
|||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import { Timeline } from "antd";
|
import { Timeline } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
export default function ScheduleExistingAppointmentsList({ existingAppointments }) {
|
const ScheduleExistingAppointmentsList = React.memo(function ScheduleExistingAppointmentsList({
|
||||||
|
existingAppointments
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (existingAppointments.loading) return <LoadingSpinner />;
|
const { loading, error, data } = existingAppointments;
|
||||||
if (existingAppointments.error) return <AlertComponent message={existingAppointments.error.message} type="error" />;
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
if (!data) return [];
|
||||||
|
return data.appointments.map((item) => ({
|
||||||
|
key: item.id,
|
||||||
|
color: item.canceled ? "red" : item.arrived ? "green" : "blue",
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{item.canceled
|
||||||
|
? t("appointments.labels.cancelledappointment")
|
||||||
|
: item.arrived
|
||||||
|
? t("appointments.labels.arrivedon")
|
||||||
|
: t("appointments.labels.scheduledfor")}
|
||||||
|
<DateTimeFormatter>{item.start}</DateTimeFormatter>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}, [data, t]);
|
||||||
|
|
||||||
|
if (loading) return <LoadingSpinner />;
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{t("appointments.labels.priorappointments")}
|
{t("appointments.labels.priorappointments")}
|
||||||
<Timeline
|
<Timeline items={items} />
|
||||||
items={
|
|
||||||
existingAppointments.data
|
|
||||||
? existingAppointments.data.appointments.map((item) => ({
|
|
||||||
key: item.id,
|
|
||||||
color: item.canceled ? "red" : item.arrived ? "green" : "blue",
|
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
{item.canceled
|
|
||||||
? t("appointments.labels.cancelledappointment")
|
|
||||||
: item.arrived
|
|
||||||
? t("appointments.labels.arrivedon")
|
|
||||||
: t("appointments.labels.scheduledfor")}
|
|
||||||
<DateTimeFormatter>{item.start}</DateTimeFormatter>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default ScheduleExistingAppointmentsList;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
|
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import React, { useState } from "react";
|
import React, { useCallback, useMemo, useState } 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";
|
||||||
@@ -19,12 +19,12 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
|
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ScheduleJobModalComponent({
|
const ScheduleJobModalComponent = React.memo(function ScheduleJobModalComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
form,
|
form,
|
||||||
existingAppointments,
|
existingAppointments,
|
||||||
@@ -36,7 +36,7 @@ export function ScheduleJobModalComponent({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [smartOptions, setSmartOptions] = useState([]);
|
const [smartOptions, setSmartOptions] = useState([]);
|
||||||
|
|
||||||
const handleSmartScheduling = async () => {
|
const handleSmartScheduling = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/scheduling/job", {
|
const response = await axios.post("/scheduling/job", {
|
||||||
@@ -48,21 +48,66 @@ export function ScheduleJobModalComponent({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [jobId]);
|
||||||
|
|
||||||
const handleDateBlur = () => {
|
const handleDateBlur = useCallback(() => {
|
||||||
const values = form.getFieldsValue();
|
const values = form.getFieldsValue();
|
||||||
|
|
||||||
if (lbrHrsData) {
|
if (lbrHrsData) {
|
||||||
const totalHours =
|
const totalHours =
|
||||||
lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs;
|
(lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs || 0) +
|
||||||
|
(lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs || 0);
|
||||||
|
|
||||||
if (values.start && !values.scheduled_completion)
|
if (values.start && !values.scheduled_completion)
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
scheduled_completion: dayjs(values.start).businessDaysAdd(totalHours / bodyshop.target_touchtime, "day")
|
scheduled_completion: dayjs(values.start).businessDaysAdd(totalHours / bodyshop.target_touchtime, "day")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, [form, lbrHrsData, bodyshop.target_touchtime]);
|
||||||
|
|
||||||
|
const colorOptions = useMemo(() => {
|
||||||
|
return (
|
||||||
|
bodyshop.appt_colors &&
|
||||||
|
bodyshop.appt_colors.map((color) => (
|
||||||
|
<Select.Option style={{ color: color.color.hex }} key={color.color.hex} value={color.color.hex}>
|
||||||
|
{color.label}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}, [bodyshop.appt_colors]);
|
||||||
|
|
||||||
|
const altTransportOptions = useMemo(() => {
|
||||||
|
return (
|
||||||
|
bodyshop.appt_alt_transport &&
|
||||||
|
bodyshop.appt_alt_transport.map((alt) => (
|
||||||
|
<Select.Option key={alt} value={alt}>
|
||||||
|
{alt}
|
||||||
|
</Select.Option>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}, [bodyshop.appt_alt_transport]);
|
||||||
|
|
||||||
|
const smartOptionsButtons = useMemo(() => {
|
||||||
|
return smartOptions.map((d, idx) => (
|
||||||
|
<Button
|
||||||
|
className="imex-flex-row__margin"
|
||||||
|
key={idx}
|
||||||
|
onClick={() => {
|
||||||
|
const ssDate = dayjs(d);
|
||||||
|
if (ssDate.isBefore(dayjs())) {
|
||||||
|
form.setFieldsValue({ start: dayjs() });
|
||||||
|
} else {
|
||||||
|
form.setFieldsValue({
|
||||||
|
start: dayjs(d).add(8, "hour")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleDateBlur();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DateFormatter includeDay>{d}</DateFormatter>
|
||||||
|
</Button>
|
||||||
|
));
|
||||||
|
}, [smartOptions, form, handleDateBlur]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
@@ -80,7 +125,6 @@ export function ScheduleJobModalComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -92,7 +136,6 @@ export function ScheduleJobModalComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -107,25 +150,7 @@ export function ScheduleJobModalComponent({
|
|||||||
<Button onClick={handleSmartScheduling} loading={loading}>
|
<Button onClick={handleSmartScheduling} loading={loading}>
|
||||||
{t("appointments.actions.calculate")}
|
{t("appointments.actions.calculate")}
|
||||||
</Button>
|
</Button>
|
||||||
{smartOptions.map((d, idx) => (
|
{smartOptionsButtons}
|
||||||
<Button
|
|
||||||
className="imex-flex-row__margin"
|
|
||||||
key={idx}
|
|
||||||
onClick={() => {
|
|
||||||
const ssDate = dayjs(d);
|
|
||||||
if (ssDate.isBefore(dayjs())) {
|
|
||||||
form.setFieldsValue({ start: dayjs() });
|
|
||||||
} else {
|
|
||||||
form.setFieldsValue({
|
|
||||||
start: dayjs(d).add(8, "hour")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
handleDateBlur();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DateFormatter includeDay>{d}</DateFormatter>
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</Space>
|
</Space>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -144,20 +169,10 @@ export function ScheduleJobModalComponent({
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
<Form.Item name="color" label={t("appointments.fields.color")}>
|
<Form.Item name="color" label={t("appointments.fields.color")}>
|
||||||
<Select allowClear>
|
<Select allowClear>{colorOptions}</Select>
|
||||||
{bodyshop.appt_colors &&
|
|
||||||
bodyshop.appt_colors.map((color) => (
|
|
||||||
<Select.Option style={{ color: color.color.hex }} key={color.color.hex} value={color.color.hex}>
|
|
||||||
{color.label}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name={"alt_transport"} label={t("jobs.fields.alt_transport")}>
|
<Form.Item name={"alt_transport"} label={t("jobs.fields.alt_transport")}>
|
||||||
<Select allowClear>
|
<Select allowClear>{altTransportOptions}</Select>
|
||||||
{bodyshop.appt_alt_transport &&
|
|
||||||
bodyshop.appt_alt_transport.map((alt) => <Select.Option key={alt}>{alt}</Select.Option>)}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name={"note"} label={t("appointments.fields.note")}>
|
<Form.Item name={"note"} label={t("appointments.fields.note")}>
|
||||||
<Input />
|
<Input />
|
||||||
@@ -183,6 +198,6 @@ export function ScheduleJobModalComponent({
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalComponent);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Form, Modal, notification } from "antd";
|
import { Form, Modal, notification } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } 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";
|
||||||
@@ -27,13 +27,21 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
scheduleModal: selectSchedule,
|
scheduleModal: selectSchedule,
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
|
||||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ScheduleJobModalContainer({
|
const ScheduleJobModalContainer = React.memo(function ScheduleJobModalContainer({
|
||||||
scheduleModal,
|
scheduleModal,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
@@ -43,168 +51,186 @@ export function ScheduleJobModalContainer({
|
|||||||
}) {
|
}) {
|
||||||
const { open, context, actions } = scheduleModal;
|
const { open, context, actions } = scheduleModal;
|
||||||
const { jobId, job, previousEvent } = context;
|
const { jobId, job, previousEvent } = context;
|
||||||
|
|
||||||
const { refetch } = actions;
|
const { refetch } = actions;
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { data: lbrHrsData } = useQuery(QUERY_LBR_HRS_BY_PK, {
|
const { data: lbrHrsData } = useQuery(QUERY_LBR_HRS_BY_PK, {
|
||||||
variables: { id: job && job.id },
|
variables: { id: job?.id },
|
||||||
skip: !job || !job.id,
|
skip: !job?.id,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
|
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
|
||||||
const [insertAppointment] = useMutation(INSERT_APPOINTMENT);
|
const [insertAppointment] = useMutation(INSERT_APPOINTMENT);
|
||||||
const [updateJobStatus] = useMutation(UPDATE_JOBS);
|
const [updateJobStatus] = useMutation(UPDATE_JOBS);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (job) form.resetFields();
|
|
||||||
}, [job, form]);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const existingAppointments = useQuery(QUERY_APPOINTMENTS_BY_JOBID, {
|
const existingAppointments = useQuery(QUERY_APPOINTMENTS_BY_JOBID, {
|
||||||
variables: { jobid: jobId },
|
variables: { jobid: jobId },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: !open || !!!jobId
|
skip: !open || !jobId
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (job) form.resetFields();
|
||||||
existingAppointments.data &&
|
}, [job, form]);
|
||||||
existingAppointments.data.appointments.length > 0 &&
|
|
||||||
!existingAppointments.data.appointments[0].canceled
|
|
||||||
) {
|
|
||||||
form.setFieldsValue({
|
|
||||||
color: existingAppointments.data.appointments[0].color,
|
|
||||||
|
|
||||||
note: existingAppointments.data.appointments[0].note
|
useEffect(() => {
|
||||||
|
const appointments = existingAppointments.data?.appointments;
|
||||||
|
if (appointments?.length && !appointments[0].canceled) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
color: appointments[0].color,
|
||||||
|
note: appointments[0].note
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [existingAppointments.data, form]);
|
}, [existingAppointments.data, form]);
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = useCallback(
|
||||||
logImEXEvent("schedule_new_appointment");
|
async (values) => {
|
||||||
|
logImEXEvent("schedule_new_appointment");
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
setLoading(true);
|
if (previousEvent) {
|
||||||
if (!!previousEvent) {
|
const cancelAppt = await cancelAppointment({
|
||||||
const cancelAppt = await cancelAppointment({
|
variables: { appid: previousEvent }
|
||||||
variables: { appid: previousEvent }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!!cancelAppt.errors) {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("appointments.errors.canceling", {
|
|
||||||
message: JSON.stringify(cancelAppt.errors)
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
notification["success"]({
|
if (cancelAppt.errors) {
|
||||||
message: t("appointments.successes.canceled")
|
notification.error({
|
||||||
});
|
message: t("appointments.errors.canceling", {
|
||||||
}
|
message: JSON.stringify(cancelAppt.errors)
|
||||||
|
})
|
||||||
if (existingAppointments.data.appointments.length > 0) {
|
|
||||||
await Promise.all(
|
|
||||||
existingAppointments.data.appointments.map((app) => {
|
|
||||||
return cancelAppointment({
|
|
||||||
variables: { appid: app.id }
|
|
||||||
});
|
});
|
||||||
})
|
return;
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const appt = await insertAppointment({
|
notification.success({
|
||||||
variables: {
|
message: t("appointments.successes.canceled")
|
||||||
app: {
|
});
|
||||||
jobid: jobId,
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
start: dayjs(values.start),
|
|
||||||
end: dayjs(values.start).add(bodyshop.appt_length || 60, "minute"),
|
|
||||||
color: values.color,
|
|
||||||
note: values.note,
|
|
||||||
created_by: currentUser.email
|
|
||||||
},
|
|
||||||
jobId: jobId,
|
|
||||||
altTransport: values.alt_transport
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (!appt.errors) {
|
const existingApps = existingAppointments.data?.appointments || [];
|
||||||
insertAuditTrail({
|
if (existingApps.length > 0) {
|
||||||
jobid: job.id,
|
await Promise.all(
|
||||||
operation: AuditTrailMapping.appointmentinsert(DateTimeFormat(values.start)),
|
existingApps.map((app) =>
|
||||||
type: "appointmentinsert"
|
cancelAppointment({
|
||||||
});
|
variables: { appid: app.id }
|
||||||
}
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!!appt.errors) {
|
const appt = await insertAppointment({
|
||||||
notification["error"]({
|
|
||||||
message: t("appointments.errors.saving", {
|
|
||||||
message: JSON.stringify(appt.errors)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
notification["success"]({
|
|
||||||
message: t("appointments.successes.created")
|
|
||||||
});
|
|
||||||
if (jobId) {
|
|
||||||
const jobUpdate = await updateJobStatus({
|
|
||||||
variables: {
|
variables: {
|
||||||
jobIds: [jobId],
|
app: {
|
||||||
fields: {
|
jobid: jobId,
|
||||||
status: bodyshop.md_ro_statuses.default_scheduled,
|
bodyshopid: bodyshop.id,
|
||||||
date_scheduled: new Date(),
|
start: dayjs(values.start),
|
||||||
scheduled_in: values.start,
|
end: dayjs(values.start).add(bodyshop.appt_length || 60, "minute"),
|
||||||
scheduled_completion: values.scheduled_completion,
|
color: values.color,
|
||||||
lost_sale_reason: null,
|
note: values.note,
|
||||||
date_lost_sale: null
|
created_by: currentUser.email
|
||||||
}
|
},
|
||||||
|
jobId: jobId,
|
||||||
|
altTransport: values.alt_transport
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!!jobUpdate.errors) {
|
if (!appt.errors) {
|
||||||
notification["error"]({
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.appointmentinsert(DateTimeFormat(values.start)),
|
||||||
|
type: "appointmentinsert"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
message: t("appointments.errors.saving", {
|
message: t("appointments.errors.saving", {
|
||||||
message: JSON.stringify(jobUpdate.errors)
|
message: JSON.stringify(appt.errors)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setLoading(false);
|
notification.success({
|
||||||
toggleModalVisible();
|
message: t("appointments.successes.created")
|
||||||
if (values.notifyCustomer) {
|
|
||||||
setEmailOptions({
|
|
||||||
jobid: jobId,
|
|
||||||
messageOptions: {
|
|
||||||
to: [values.email],
|
|
||||||
replyTo: bodyshop.email,
|
|
||||||
subject: TemplateList("appointment").appointment_confirmation.subject
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
name: TemplateList("appointment").appointment_confirmation.key,
|
|
||||||
variables: {
|
|
||||||
id: appt.data.insert_appointments.returning[0].id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
if (refetch) refetch();
|
if (jobId) {
|
||||||
};
|
const jobUpdate = await updateJobStatus({
|
||||||
|
variables: {
|
||||||
|
jobIds: [jobId],
|
||||||
|
fields: {
|
||||||
|
status: bodyshop.md_ro_statuses.default_scheduled,
|
||||||
|
date_scheduled: new Date(),
|
||||||
|
scheduled_in: values.start,
|
||||||
|
scheduled_completion: values.scheduled_completion,
|
||||||
|
lost_sale_reason: null,
|
||||||
|
date_lost_sale: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (jobUpdate.errors) {
|
||||||
|
notification.error({
|
||||||
|
message: t("appointments.errors.saving", {
|
||||||
|
message: JSON.stringify(jobUpdate.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.notifyCustomer) {
|
||||||
|
setEmailOptions({
|
||||||
|
jobid: jobId,
|
||||||
|
messageOptions: {
|
||||||
|
to: [values.email],
|
||||||
|
replyTo: bodyshop.email,
|
||||||
|
subject: TemplateList("appointment").appointment_confirmation.subject
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
name: TemplateList("appointment").appointment_confirmation.key,
|
||||||
|
variables: {
|
||||||
|
id: appt.data.insert_appointments.returning[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refetch) refetch();
|
||||||
|
toggleModalVisible();
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
t,
|
||||||
|
previousEvent,
|
||||||
|
cancelAppointment,
|
||||||
|
existingAppointments.data,
|
||||||
|
insertAppointment,
|
||||||
|
jobId,
|
||||||
|
bodyshop.id,
|
||||||
|
bodyshop.appt_length,
|
||||||
|
currentUser.email,
|
||||||
|
insertAuditTrail,
|
||||||
|
job,
|
||||||
|
updateJobStatus,
|
||||||
|
bodyshop.md_ro_statuses.default_scheduled,
|
||||||
|
setEmailOptions,
|
||||||
|
refetch,
|
||||||
|
toggleModalVisible,
|
||||||
|
bodyshop.email
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={toggleModalVisible}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
width={"90%"}
|
width="90%"
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
okButtonProps={{
|
okButtonProps={{
|
||||||
@@ -217,10 +243,9 @@ export function ScheduleJobModalContainer({
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
notifyCustomer: !!(job && job.ownr_ea),
|
notifyCustomer: !!job?.ownr_ea,
|
||||||
email: (job && job.ownr_ea) || "",
|
email: job?.ownr_ea || "",
|
||||||
start: null,
|
start: null,
|
||||||
// smartDates: [],
|
|
||||||
scheduled_completion: null,
|
scheduled_completion: null,
|
||||||
color: context.color,
|
color: context.color,
|
||||||
alt_transport: context.alt_transport,
|
alt_transport: context.alt_transport,
|
||||||
@@ -236,6 +261,6 @@ export function ScheduleJobModalContainer({
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalContainer);
|
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalContainer);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Form, Input, Popover, Select, Space } from "antd";
|
import { Button, Card, Form, Input, Popover, Select, Space } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } 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";
|
||||||
@@ -13,142 +13,143 @@ import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleManualEvent);
|
|
||||||
|
|
||||||
export function ScheduleManualEvent({ bodyshop, event }) {
|
const ScheduleManualEvent = React.memo(function ScheduleManualEvent({ bodyshop, event }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT, {
|
||||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
||||||
|
});
|
||||||
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT, {
|
||||||
|
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
||||||
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
// const [callQuery, { loading: entryLoading, data: entryData }] = useLazyQuery(
|
|
||||||
// QUERY_SCOREBOARD_ENTRY
|
const handleFinish = useCallback(
|
||||||
// );
|
async (values) => {
|
||||||
|
logImEXEvent("schedule_manual_event");
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (event && event.id) {
|
||||||
|
await updateAppointment({
|
||||||
|
variables: { appid: event.id, app: values }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await insertAppointment({
|
||||||
|
variables: {
|
||||||
|
apt: {
|
||||||
|
...values,
|
||||||
|
isintake: false,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
form.resetFields();
|
||||||
|
setVisibility(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[event, updateAppointment, insertAppointment, bodyshop.id, form]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
setVisibility(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visibility && event) {
|
if (visibility && event) {
|
||||||
form.setFieldsValue(event);
|
form.setFieldsValue(event);
|
||||||
|
} else if (!visibility) {
|
||||||
|
form.resetFields();
|
||||||
}
|
}
|
||||||
}, [visibility, form, event]);
|
}, [visibility, form, event]);
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const colorOptions = useMemo(() => {
|
||||||
logImEXEvent("schedule_manual_event");
|
return bodyshop.appt_colors.map((col, idx) => (
|
||||||
|
<Select.Option key={idx} value={col.color.hex}>
|
||||||
setLoading(true);
|
{col.label}
|
||||||
try {
|
</Select.Option>
|
||||||
if (event && event.id) {
|
));
|
||||||
updateAppointment({
|
}, [bodyshop.appt_colors]);
|
||||||
variables: { appid: event.id, app: values },
|
|
||||||
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
insertAppointment({
|
|
||||||
variables: {
|
|
||||||
apt: { ...values, isintake: false, bodyshopid: bodyshop.id }
|
|
||||||
},
|
|
||||||
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
form.resetFields();
|
|
||||||
setVisibility(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const overlay = (
|
const overlay = (
|
||||||
<Card>
|
<Card>
|
||||||
<div>
|
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
||||||
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
<Form.Item
|
||||||
<Form.Item
|
label={t("appointments.fields.title")}
|
||||||
label={t("appointments.fields.title")}
|
name="title"
|
||||||
name="title"
|
rules={[
|
||||||
rules={[
|
{
|
||||||
{
|
required: true
|
||||||
required: true
|
}
|
||||||
//message: t("general.validation.required"),
|
]}
|
||||||
}
|
>
|
||||||
]}
|
<Input />
|
||||||
>
|
</Form.Item>
|
||||||
<Input />
|
<Form.Item label={t("appointments.fields.note")} name="note">
|
||||||
</Form.Item>
|
<Input />
|
||||||
<Form.Item label={t("appointments.fields.note")} name="note">
|
</Form.Item>
|
||||||
<Input />
|
<Form.Item
|
||||||
</Form.Item>
|
label={t("appointments.fields.start")}
|
||||||
<Form.Item
|
name="start"
|
||||||
label={t("appointments.fields.start")}
|
rules={[
|
||||||
name="start"
|
{
|
||||||
rules={[
|
required: true
|
||||||
{
|
}
|
||||||
required: true
|
]}
|
||||||
//message: t("general.validation.required"),
|
>
|
||||||
}
|
<FormDateTimePickerComponent />
|
||||||
]}
|
</Form.Item>
|
||||||
>
|
<Form.Item
|
||||||
<FormDateTimePickerComponent />
|
label={t("appointments.fields.end")}
|
||||||
</Form.Item>
|
name="end"
|
||||||
<Form.Item
|
rules={[
|
||||||
label={t("appointments.fields.end")}
|
{
|
||||||
name="end"
|
required: true
|
||||||
rules={[
|
},
|
||||||
{
|
({ getFieldValue }) => ({
|
||||||
required: true
|
validator(rule, value) {
|
||||||
//message: t("general.validation.required"),
|
if (value) {
|
||||||
},
|
const start = form.getFieldValue("start");
|
||||||
({ getFieldValue }) => ({
|
if (dayjs(start).isAfter(dayjs(value))) {
|
||||||
async validator(rule, value) {
|
return Promise.reject(t("employees.labels.endmustbeafterstart"));
|
||||||
if (value) {
|
|
||||||
const { start } = form.getFieldsValue();
|
|
||||||
if (dayjs(start).isAfter(dayjs(value))) {
|
|
||||||
return Promise.reject(t("employees.labels.endmustbeafterstart"));
|
|
||||||
} else {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
]}
|
})
|
||||||
>
|
]}
|
||||||
<FormDateTimePickerComponent />
|
>
|
||||||
</Form.Item>
|
<FormDateTimePickerComponent />
|
||||||
<Form.Item label={t("appointments.fields.color")} name="color">
|
</Form.Item>
|
||||||
<Select>
|
<Form.Item label={t("appointments.fields.color")} name="color">
|
||||||
{bodyshop.appt_colors.map((col, idx) => (
|
<Select>{colorOptions}</Select>
|
||||||
<Select.Option key={idx} value={col.color.hex}>
|
</Form.Item>
|
||||||
{col.label}
|
<Space wrap>
|
||||||
</Select.Option>
|
<Button type="primary" htmlType="submit" loading={loading}>
|
||||||
))}
|
{t("general.actions.save")}
|
||||||
</Select>
|
</Button>
|
||||||
</Form.Item>
|
<Button onClick={() => setVisibility(false)}>{t("general.actions.cancel")}</Button>
|
||||||
|
</Space>
|
||||||
<Space wrap>
|
</Form>
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
{t("general.actions.save")}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setVisibility(false)}>{t("general.actions.cancel")}</Button>
|
|
||||||
</Space>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = (e) => {
|
|
||||||
setVisibility(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={overlay} open={visibility}>
|
<Popover content={overlay} open={visibility}>
|
||||||
<Button loading={loading} onClick={handleClick}>
|
<Button onClick={handleClick}>
|
||||||
{event ? t("appointments.actions.reschedule") : t("appointments.labels.manualevent")}
|
{event ? t("appointments.actions.reschedule") : t("appointments.labels.manualevent")}
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ScheduleManualEvent);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DownOutlined } from "@ant-design/icons";
|
import { DownOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Popover } from "antd";
|
import { Button, Card, Popover } from "antd";
|
||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
import { useLazyQuery } from "@apollo/client";
|
import { useLazyQuery } from "@apollo/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@@ -11,52 +11,52 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
|||||||
import "./schedule-production-list.styles.scss";
|
import "./schedule-production-list.styles.scss";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
|
||||||
export default function ScheduleProductionList() {
|
const ScheduleProductionList = React.memo(function ScheduleProductionList() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [callQuery, { loading, error, data }] = useLazyQuery(QUERY_JOBS_IN_PRODUCTION);
|
const [callQuery, { loading, error, data }] = useLazyQuery(QUERY_JOBS_IN_PRODUCTION);
|
||||||
|
|
||||||
const content = () => {
|
const content = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div onClick={(e) => e.stopPropagation()} className="jobs-in-production-table">
|
<div onClick={(e) => e.stopPropagation()} className="jobs-in-production-table">
|
||||||
{loading ? <LoadingSkeleton /> : null}
|
{loading && <LoadingSkeleton />}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error && <AlertComponent message={error.message} type="error" />}
|
||||||
{data ? (
|
{data && data.jobs && (
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data && data.jobs
|
{data.jobs.map((j) => (
|
||||||
? data.jobs.map((j) => (
|
<tr key={j.id}>
|
||||||
<tr key={j.id}>
|
<td>
|
||||||
<td>
|
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<OwnerNameDisplay ownerObject={j} />
|
||||||
<OwnerNameDisplay ownerObject={j} />
|
</td>
|
||||||
</td>
|
<td>{`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}</td>
|
||||||
<td>{`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}</td>
|
<td>{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${
|
||||||
<td>{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${
|
j.larhrs.aggregate.sum.mod_lb_hrs || "0"
|
||||||
j.larhrs.aggregate.sum.mod_lb_hrs || "0"
|
}`}</td>
|
||||||
}`}</td>
|
<td>
|
||||||
<td>
|
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
||||||
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
))}
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
}, [loading, error, data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={content} trigger="click" placement="bottomRight">
|
<Popover content={content} trigger="click" placement="bottomRight">
|
||||||
<Button onClick={() => callQuery()}>
|
<Button onClick={callQuery}>
|
||||||
{t("appointments.labels.inproduction")}
|
{t("appointments.labels.inproduction")}
|
||||||
<DownOutlined />
|
<DownOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default ScheduleProductionList;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { Button } from "antd";
|
import { Button } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import React, { useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
|
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
|
||||||
@@ -10,49 +10,46 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
||||||
});
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleVerifyIntegrity);
|
|
||||||
|
|
||||||
export function ScheduleVerifyIntegrity({ currentUser }) {
|
const ScheduleVerifyIntegrity = React.memo(function ScheduleVerifyIntegrity({ currentUser }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const handleVerify = async () => {
|
|
||||||
|
const handleVerify = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const {
|
try {
|
||||||
data: { arrJobs, compJobs, prodJobs }
|
const {
|
||||||
} = await client.query({
|
data: { arrJobs, compJobs, prodJobs }
|
||||||
query: QUERY_SCHEDULE_LOAD_DATA,
|
} = await client.query({
|
||||||
variables: { start: dayjs(), end: dayjs().add(180, "day") }
|
query: QUERY_SCHEDULE_LOAD_DATA,
|
||||||
});
|
variables: { start: dayjs(), end: dayjs().add(180, "day") }
|
||||||
|
});
|
||||||
|
|
||||||
//check that the leaving jobs are either in the arriving list, or in production.
|
// Check that the completing jobs are either in production or arriving within the next 180 days.
|
||||||
const issues = [];
|
const issues = compJobs.filter((j) => {
|
||||||
|
const inProdJobs = prodJobs.some((p) => p.id === j.id);
|
||||||
|
const inArrJobs = arrJobs.some((p) => p.id === j.id);
|
||||||
|
return !(inProdJobs || inArrJobs);
|
||||||
|
});
|
||||||
|
|
||||||
compJobs.forEach((j) => {
|
console.log("The following completing jobs are not in production or arriving within the next 180 days:", issues);
|
||||||
const inProdJobs = prodJobs.find((p) => p.id === j.id);
|
} catch (error) {
|
||||||
const inArrJobs = arrJobs.find((p) => p.id === j.id);
|
console.error("Error verifying schedule integrity:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
if (!(inProdJobs || inArrJobs)) {
|
// TODO: A Global helper with developer emails
|
||||||
// NOT FOUND!
|
if (currentUser.email !== "patrick@imex.prod") {
|
||||||
issues.push(j);
|
return null;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
console.log(
|
|
||||||
"The following completing jobs are not in production, or are arriving within the next 180 days. ",
|
|
||||||
issues
|
|
||||||
);
|
|
||||||
|
|
||||||
setLoading(false);
|
return (
|
||||||
};
|
<Button loading={loading} onClick={handleVerify}>
|
||||||
|
Developer Use Only - Verify Schedule Integrity
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (currentUser.email === "patrick@imex.prod")
|
export default connect(mapStateToProps)(ScheduleVerifyIntegrity);
|
||||||
return (
|
|
||||||
<Button loading={loading} onClick={handleVerify}>
|
|
||||||
Developer Use Only - Verify Schedule Integrity
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
else return null;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user