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:
Dave Richer
2024-09-16 23:02:20 -04:00
parent 449330441a
commit 10ba19f0d2
16 changed files with 886 additions and 791 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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