IO-2932-Scheduling-Lag-on-AIO:
Full Optimization of all Schedule related components. Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
@@ -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,71 @@ 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 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 +158,23 @@ 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={{
|
||||||
event: (e) => Event({ bodyshop: bodyshop, event: e.event, refetch: refetch }),
|
event: eventComponent,
|
||||||
header: (p) => <HeaderComponent {...p} events={data} refetch={refetch} />
|
header: headerComponent
|
||||||
}}
|
}}
|
||||||
{...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, { 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,6 +60,70 @@ 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 (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<ScheduleModal />
|
<ScheduleModal />
|
||||||
@@ -76,65 +138,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>
|
||||||
}
|
}
|
||||||
@@ -148,4 +180,6 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
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