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 React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectScheduleLoad } from "../../redux/application/application.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
scheduleLoad: selectScheduleLoad
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ScheduleAtsSummary({ scheduleLoad, appointments }) {
|
||||
const ScheduleAtsSummary = React.memo(function ScheduleAtsSummary({ appointments }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const atsSummary = useMemo(() => {
|
||||
let atsSummary = {};
|
||||
if (!appointments || appointments.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const summary = {};
|
||||
appointments
|
||||
.filter((a) => a.isintake)
|
||||
.filter((a) => a.isintake && a.job?.alt_transport)
|
||||
.forEach((a) => {
|
||||
if (!a.job.alt_transport) return;
|
||||
if (!atsSummary[a.job.alt_transport]) {
|
||||
atsSummary[a.job.alt_transport] = 1;
|
||||
} else {
|
||||
atsSummary[a.job.alt_transport] = atsSummary[a.job.alt_transport] + 1;
|
||||
}
|
||||
const key = a.job.alt_transport;
|
||||
summary[key] = (summary[key] || 0) + 1;
|
||||
});
|
||||
return atsSummary;
|
||||
return summary;
|
||||
}, [appointments]);
|
||||
|
||||
if (Object.keys(atsSummary).length > 0)
|
||||
if (Object.keys(atsSummary).length > 0) {
|
||||
return (
|
||||
<Space wrap>
|
||||
{t("schedule.labels.atssummary")}
|
||||
{Object.keys(atsSummary).map((key) => (
|
||||
<span key={key}>{`${key}: ${atsSummary[key]}`}</span>
|
||||
{Object.entries(atsSummary).map(([key, value]) => (
|
||||
<span key={key}>{`${key}: ${value}`}</span>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleAtsSummary);
|
||||
export default ScheduleAtsSummary;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Dropdown, notification } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -13,57 +13,61 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function ScheduleBlockDay({ date, children, refetch, bodyshop, alreadyBlocked }) {
|
||||
const ScheduleBlockDay = React.memo(function ScheduleBlockDay({ date, children, refetch, bodyshop, alreadyBlocked }) {
|
||||
const { t } = useTranslation();
|
||||
const [insertBlock] = useMutation(INSERT_APPOINTMENT_BLOCK);
|
||||
|
||||
const handleMenu = async (e) => {
|
||||
e.domEvent.stopPropagation();
|
||||
const handleMenu = useCallback(
|
||||
async (e) => {
|
||||
e.domEvent.stopPropagation();
|
||||
|
||||
if (e.key === "block") {
|
||||
const blockAppt = {
|
||||
title: t("appointments.labels.blocked"),
|
||||
block: true,
|
||||
isintake: false,
|
||||
bodyshopid: bodyshop.id,
|
||||
start: dayjs(date).startOf("day"),
|
||||
end: dayjs(date).endOf("day")
|
||||
};
|
||||
logImEXEvent("dashboard_change_layout");
|
||||
if (e.key === "block") {
|
||||
const blockAppt = {
|
||||
title: t("appointments.labels.blocked"),
|
||||
block: true,
|
||||
isintake: false,
|
||||
bodyshopid: bodyshop.id,
|
||||
start: dayjs(date).startOf("day"),
|
||||
end: dayjs(date).endOf("day")
|
||||
};
|
||||
logImEXEvent("dashboard_change_layout");
|
||||
|
||||
const result = await insertBlock({
|
||||
variables: { app: [blockAppt] }
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.blocking", {
|
||||
message: JSON.stringify(result.errors)
|
||||
})
|
||||
const result = await insertBlock({
|
||||
variables: { app: [blockAppt] }
|
||||
});
|
||||
}
|
||||
|
||||
if (!!refetch) refetch();
|
||||
}
|
||||
};
|
||||
if (result.errors) {
|
||||
notification.error({
|
||||
message: t("appointments.errors.blocking", {
|
||||
message: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const menu = {
|
||||
items: [
|
||||
{
|
||||
key: "block",
|
||||
label: t("appointments.actions.block")
|
||||
if (refetch) refetch();
|
||||
}
|
||||
],
|
||||
onClick: handleMenu
|
||||
};
|
||||
},
|
||||
[t, bodyshop.id, date, insertBlock, refetch]
|
||||
);
|
||||
|
||||
const menu = useMemo(
|
||||
() => ({
|
||||
items: [
|
||||
{
|
||||
key: "block",
|
||||
label: t("appointments.actions.block")
|
||||
}
|
||||
],
|
||||
onClick: handleMenu
|
||||
}),
|
||||
[t, handleMenu]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown menu={menu} disabled={alreadyBlocked} trigger={["contextMenu"]}>
|
||||
{children}
|
||||
</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({
|
||||
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 { 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 {
|
||||
bucket: loadData.expectedLoad[key].label,
|
||||
current: loadData.expectedLoad[key].count,
|
||||
target: metadataBucket && metadataBucket.target
|
||||
};
|
||||
})) ||
|
||||
[]
|
||||
);
|
||||
const data = useMemo(() => {
|
||||
if (!loadData || !loadData.expectedLoad || !ssbuckets) return [];
|
||||
|
||||
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]);
|
||||
|
||||
const popContent = (
|
||||
<div>
|
||||
<Space>
|
||||
{t("appointments.labels.expectedprodhrs")}
|
||||
<strong>{loadData?.expectedHours?.toFixed(1)}</strong>
|
||||
{t("appointments.labels.expectedjobs")}
|
||||
<strong>{loadData?.expectedJobCount}</strong>
|
||||
</Space>
|
||||
<RadarChart
|
||||
// cx={300}
|
||||
// cy={250}
|
||||
// outerRadius={150}
|
||||
width={800}
|
||||
height={600}
|
||||
data={data}
|
||||
>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="bucket" />
|
||||
<PolarRadiusAxis angle={90} />
|
||||
<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} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</RadarChart>
|
||||
</div>
|
||||
const popContent = useMemo(
|
||||
() => (
|
||||
<div>
|
||||
<Space>
|
||||
{t("appointments.labels.expectedprodhrs")}
|
||||
<strong>{loadData?.expectedHours?.toFixed(1) || 0}</strong>
|
||||
{t("appointments.labels.expectedjobs")}
|
||||
<strong>{loadData?.expectedJobCount || 0}</strong>
|
||||
</Space>
|
||||
<RadarChart width={300} height={250} data={data}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="bucket" />
|
||||
<PolarRadiusAxis angle={90} />
|
||||
<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} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</RadarChart>
|
||||
</div>
|
||||
),
|
||||
[t, loadData, data]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -66,6 +59,6 @@ export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) {
|
||||
<RadarChartOutlined />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderGraph);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import Icon from "@ant-design/icons";
|
||||
import { Popover, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdFileDownload, MdFileUpload } from "react-icons/md";
|
||||
import { connect } from "react-redux";
|
||||
@@ -24,115 +23,114 @@ const mapStateToProps = createStructuredSelector({
|
||||
calculating: selectScheduleLoadCalculating
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export function ScheduleCalendarHeaderComponent({
|
||||
export const ScheduleCalendarHeaderComponent = React.memo(function ScheduleCalendarHeaderComponent({
|
||||
bodyshop,
|
||||
label,
|
||||
refetch,
|
||||
date,
|
||||
load,
|
||||
calculating,
|
||||
events,
|
||||
...otherProps
|
||||
events
|
||||
}) {
|
||||
const dayjsDate = useMemo(() => dayjs(date), [date]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ATSToday = useMemo(() => {
|
||||
if (!events) return [];
|
||||
return _.groupBy(
|
||||
events.filter((e) => !e.vacation && e.isintake && dayjs(date).isSame(dayjs(e.start), "day")),
|
||||
"job.alt_transport"
|
||||
);
|
||||
}, [events, date]);
|
||||
const filteredEvents = events.filter((e) => !e.vacation && e.isintake && dayjsDate.isSame(dayjs(e.start), "day"));
|
||||
return _.groupBy(filteredEvents, "job.alt_transport");
|
||||
}, [events, dayjsDate]);
|
||||
|
||||
const isDayBlocked = useMemo(() => {
|
||||
if (!events) return [];
|
||||
return events && events.filter((e) => dayjs(date).isSame(dayjs(e.start), "day") && e.block);
|
||||
}, [events, date]);
|
||||
return events.filter((e) => dayjsDate.isSame(dayjs(e.start), "day") && e.block);
|
||||
}, [events, dayjsDate]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const loadData = load[date.toISOString().substr(0, 10)];
|
||||
const dateString = dayjsDate.format("YYYY-MM-DD");
|
||||
const loadData = load[dateString];
|
||||
|
||||
const jobsOutPopup = () => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<table>
|
||||
<tbody>
|
||||
{loadData && loadData.allJobsOut ? (
|
||||
loadData.allJobsOut.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> (
|
||||
{j.status})
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${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
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>
|
||||
{j.scheduled_completion}
|
||||
</DateTimeFormatter>
|
||||
</td>
|
||||
const jobsOutPopup = useCallback(
|
||||
() => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<table>
|
||||
<tbody>
|
||||
{loadData && loadData.allJobsOut && loadData.allJobsOut.length > 0 ? (
|
||||
loadData.allJobsOut.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link> ({j.status})
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${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 || 0) + (j.larhrs?.aggregate?.sum?.mod_lb_hrs || 0)
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td style={{ padding: "2.5px" }}>{t("appointments.labels.nocompletingjobs")}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{t("appointments.labels.nocompletingjobs")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
[loadData, t]
|
||||
);
|
||||
|
||||
const jobsInPopup = () => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<table>
|
||||
<tbody>
|
||||
{loadData && loadData.allJobsIn ? (
|
||||
loadData.allJobsIn.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${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
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
|
||||
</td>
|
||||
const jobsInPopup = useCallback(
|
||||
() => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<table>
|
||||
<tbody>
|
||||
{loadData && loadData.allJobsIn && loadData.allJobsIn.length > 0 ? (
|
||||
loadData.allJobsIn.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{`(${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 || 0) + (j.larhrs?.aggregate?.sum?.mod_lb_hrs || 0)
|
||||
).toFixed(1)} ${t("general.labels.hours")})`}
|
||||
</td>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
<DateTimeFormatter>{j.scheduled_in}</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td style={{ padding: "2.5px" }}>{t("appointments.labels.noarrivingjobs")}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td style={{ padding: "2.5px" }}>
|
||||
{t("appointments.labels.noarrivingjobs")}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
[loadData, t]
|
||||
);
|
||||
|
||||
const LoadComponent = loadData ? (
|
||||
<div>
|
||||
const LoadComponent = useMemo(() => {
|
||||
if (!loadData) return null;
|
||||
return (
|
||||
<div>
|
||||
<Space align="center">
|
||||
<Popover
|
||||
placement={"bottom"}
|
||||
@@ -141,12 +139,8 @@ export function ScheduleCalendarHeaderComponent({
|
||||
title={t("appointments.labels.arrivingjobs")}
|
||||
>
|
||||
<Icon component={MdFileDownload} style={{ color: "green" }} />
|
||||
{(loadData.allHoursInBody || 0) &&
|
||||
loadData.allHoursInBody.toFixed(1)}
|
||||
/
|
||||
{(loadData.allHoursInRefinish || 0) &&
|
||||
loadData.allHoursInRefinish.toFixed(1)}
|
||||
/{(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)}
|
||||
{(loadData.allHoursInBody || 0).toFixed(1)}/{(loadData.allHoursInRefinish || 0).toFixed(1)}/
|
||||
{(loadData.allHoursIn || 0).toFixed(1)}
|
||||
</Popover>
|
||||
<Popover
|
||||
placement={"bottom"}
|
||||
@@ -155,57 +149,31 @@ export function ScheduleCalendarHeaderComponent({
|
||||
title={t("appointments.labels.completingjobs")}
|
||||
>
|
||||
<Icon component={MdFileUpload} style={{ color: "red" }} />
|
||||
{(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(1)}
|
||||
{(loadData.allHoursOut || 0).toFixed(1)}
|
||||
</Popover>
|
||||
<ScheduleCalendarHeaderGraph loadData={loadData} />
|
||||
</Space>
|
||||
|
||||
<div>
|
||||
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>
|
||||
{Object.keys(ATSToday).map((key, idx) => (
|
||||
<li key={idx}>{`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<ul style={{ listStyleType: "none", columns: "2 auto", padding: 0 }}>
|
||||
{Object.keys(ATSToday).map((key, idx) => (
|
||||
<li key={idx}>{`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
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;
|
||||
}
|
||||
);
|
||||
}, [loadData, jobsInPopup, jobsOutPopup, t, ATSToday]);
|
||||
|
||||
const isShopOpen = useCallback(() => {
|
||||
const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
||||
const day = days[dayjsDate.day()];
|
||||
return bodyshop.workingdays[day];
|
||||
};
|
||||
}, [bodyshop, dayjsDate]);
|
||||
|
||||
return (
|
||||
<div className="imex-calendar-load">
|
||||
<ScheduleBlockDay alreadyBlocked={isDayBlocked.length > 0} date={date} refetch={refetch}>
|
||||
<div style={{ color: isShopOpen(date) ? "" : "tomato" }}>
|
||||
<div style={{ color: isShopOpen() ? "" : "tomato" }}>
|
||||
{label}
|
||||
{InstanceRenderMgr({
|
||||
imex: calculating ? <LoadingSkeleton /> : LoadComponent,
|
||||
@@ -216,6 +184,6 @@ export function ScheduleCalendarHeaderComponent({
|
||||
</ScheduleBlockDay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderComponent);
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
export function getRange(dateParam, viewParam) {
|
||||
let start, end;
|
||||
let date = dateParam || new Date();
|
||||
let view = viewParam || "week";
|
||||
// if view is day: from dayjs(date).startOf('day') to dayjs(date).endOf('day');
|
||||
if (view === "day") {
|
||||
start = dayjs(date).startOf("day");
|
||||
end = dayjs(date).endOf("day");
|
||||
}
|
||||
// if view is week: from dayjs(date).startOf('isoWeek') to dayjs(date).endOf('isoWeek');
|
||||
else if (view === "week") {
|
||||
start = dayjs(date).startOf("week");
|
||||
end = dayjs(date).endOf("week");
|
||||
}
|
||||
//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") {
|
||||
start = dayjs(date).startOf("month").subtract(7, "day");
|
||||
end = dayjs(date).endOf("month").add(7, "day");
|
||||
}
|
||||
// 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");
|
||||
}
|
||||
// Predefine range calculation functions for each view
|
||||
const viewRanges = {
|
||||
day: (date) => ({
|
||||
start: date.startOf("day"),
|
||||
end: date.endOf("day")
|
||||
}),
|
||||
week: (date) => ({
|
||||
start: date.startOf("week"),
|
||||
end: date.endOf("week")
|
||||
}),
|
||||
month: (date) => ({
|
||||
// Adjusting for adjacent weeks
|
||||
start: date.startOf("month").subtract(7, "day"),
|
||||
end: date.endOf("month").add(7, "day")
|
||||
}),
|
||||
agenda: (date) => ({
|
||||
start: date.startOf("day"),
|
||||
end: 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 queryString from "query-string";
|
||||
import React from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { Calendar, dayjsLocalizer } from "react-big-calendar";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
@@ -19,9 +19,10 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
problemJobs: selectProblemJobs
|
||||
});
|
||||
|
||||
const localizer = dayjsLocalizer(dayjs);
|
||||
|
||||
export function ScheduleCalendarWrapperComponent({
|
||||
export const ScheduleCalendarWrapperComponent = React.memo(function ScheduleCalendarWrapperComponent({
|
||||
bodyshop,
|
||||
problemJobs,
|
||||
data,
|
||||
@@ -31,23 +32,71 @@ export function ScheduleCalendarWrapperComponent({
|
||||
date,
|
||||
...otherProps
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
const location = useLocation();
|
||||
const search = useMemo(() => queryString.parse(location.search), [location.search]);
|
||||
const navigate = useNavigate();
|
||||
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 (
|
||||
<>
|
||||
@@ -109,32 +158,23 @@ export function ScheduleCalendarWrapperComponent({
|
||||
events={data}
|
||||
defaultView={search.view || defaultView || "week"}
|
||||
date={selectedDate}
|
||||
onNavigate={(date, view, action) => {
|
||||
search.date = date.toISOString().substr(0, 10);
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
onRangeChange={(start, end) => {
|
||||
if (setDateRangeCallback) setDateRangeCallback({ start, end });
|
||||
}}
|
||||
onView={(view) => {
|
||||
search.view = view;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}}
|
||||
onNavigate={onNavigate}
|
||||
onRangeChange={onRangeChange}
|
||||
onView={onView}
|
||||
step={15}
|
||||
// timeslots={1}
|
||||
showMultiDayTimes
|
||||
localizer={localizer}
|
||||
min={bodyshop.schedule_start_time ? new Date(bodyshop.schedule_start_time) : new Date("2020-01-01T06:00:00")}
|
||||
max={bodyshop.schedule_end_time ? new Date(bodyshop.schedule_end_time) : new Date("2020-01-01T20:00:00")}
|
||||
min={minTime}
|
||||
max={maxTime}
|
||||
eventPropGetter={handleEventPropStyles}
|
||||
components={{
|
||||
event: (e) => Event({ bodyshop: bodyshop, event: e.event, refetch: refetch }),
|
||||
header: (p) => <HeaderComponent {...p} events={data} refetch={refetch} />
|
||||
event: eventComponent,
|
||||
header: headerComponent
|
||||
}}
|
||||
{...otherProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(ScheduleCalendarWrapperComponent);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Checkbox, Col, Row, Select, Space } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { t } from "i18next";
|
||||
import React, { useMemo } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import ScheduleAtsSummary from "../schedule-ats-summary/schedule-ats-summary.component";
|
||||
import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/scheduler-calendar-wrapper.component";
|
||||
@@ -18,19 +18,17 @@ import _ from "lodash";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
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", {
|
||||
intake: true,
|
||||
manual: true,
|
||||
employeevacation: true,
|
||||
ins_co_nm: null
|
||||
});
|
||||
const [estimatorsFilter, setEstimatiorsFilter] = useLocalStorage("estimators", []);
|
||||
const [estimatorsFilter, setEstimatorsFilter] = useLocalStorage("estimators", []);
|
||||
|
||||
const estimators = useMemo(() => {
|
||||
return _.uniq([
|
||||
@@ -48,7 +46,7 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
d.__typename === "appointments"
|
||||
? estimatorsFilter.length === 0
|
||||
? 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;
|
||||
|
||||
return (
|
||||
@@ -62,6 +60,70 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
});
|
||||
}, [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 (
|
||||
<Row gutter={[16, 16]}>
|
||||
<ScheduleModal />
|
||||
@@ -76,65 +138,35 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
mode="multiple"
|
||||
placeholder={t("schedule.labels.estimators")}
|
||||
allowClear
|
||||
onClear={() => setEstimatiorsFilter([])}
|
||||
value={[...estimatorsFilter]}
|
||||
onChange={(e) => {
|
||||
setEstimatiorsFilter(e);
|
||||
}}
|
||||
options={estimators.map((e) => ({
|
||||
label: e,
|
||||
value: e
|
||||
}))}
|
||||
onClear={handleEstimatorsFilterClear}
|
||||
value={estimatorsFilter}
|
||||
onChange={handleEstimatorsFilterChange}
|
||||
options={estimatorsOptions}
|
||||
/>
|
||||
<Select
|
||||
style={{ minWidth: "15rem" }}
|
||||
mode="multiple"
|
||||
placeholder={t("schedule.labels.ins_co_nm_filter")}
|
||||
allowClear
|
||||
onClear={() => setFilter({ ...filter, ins_co_nm: [] })}
|
||||
value={filter?.ins_co_nm ? filter.ins_co_nm : []}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, ins_co_nm: e });
|
||||
}}
|
||||
options={bodyshop.md_ins_cos.map((i) => ({
|
||||
label: i.name,
|
||||
value: i.name
|
||||
}))}
|
||||
onClear={handleInsCoNmFilterClear}
|
||||
value={filter.ins_co_nm || []}
|
||||
onChange={handleInsCoNmFilterChange}
|
||||
options={insCoNmOptions}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={filter?.intake}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, intake: e.target.checked });
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={filter.intake} onChange={handleIntakeFilterChange}>
|
||||
{t("schedule.labels.intake")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={filter?.manual}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, manual: e.target.checked });
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={filter.manual} onChange={handleManualFilterChange}>
|
||||
{t("schedule.labels.manual")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={filter?.employeevacation}
|
||||
onChange={(e) => {
|
||||
setFilter({ ...filter, employeevacation: e.target.checked });
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={filter.employeevacation} onChange={handleEmployeeVacationFilterChange}>
|
||||
{t("schedule.labels.employeevacation")}
|
||||
</Checkbox>
|
||||
<ScheduleVerifyIntegrity />
|
||||
<Button
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button onClick={handleRefetch}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<ScheduleProductionList />
|
||||
|
||||
<ScheduleManualEvent />
|
||||
</Space>
|
||||
}
|
||||
@@ -148,4 +180,6 @@ export function ScheduleCalendarComponent({ data, refetch, bodyshop }) {
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(ScheduleCalendarComponent);
|
||||
|
||||
@@ -15,56 +15,65 @@ import dayjs from "../../utils/day";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
|
||||
});
|
||||
|
||||
export function ScheduleCalendarContainer({ calculateScheduleLoad }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const ScheduleCalendarContainer = React.memo(function ScheduleCalendarContainer({ calculateScheduleLoad }) {
|
||||
const location = useLocation();
|
||||
const search = useMemo(() => queryString.parse(location.search), [location.search]);
|
||||
|
||||
const { date, view } = search;
|
||||
const range = useMemo(() => getRange(date, view), [date, view]);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_APPOINTMENTS, {
|
||||
variables: {
|
||||
const queryVariables = useMemo(
|
||||
() => ({
|
||||
start: range.start.toDate(),
|
||||
end: range.end.toDate(),
|
||||
startd: range.start,
|
||||
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",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && range.end) calculateScheduleLoad(range.end);
|
||||
}, [data, range, calculateScheduleLoad]);
|
||||
if (data && range.end) {
|
||||
calculateScheduleLoad(range.end);
|
||||
}
|
||||
}, [data, range.end, calculateScheduleLoad]);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
let normalizedData = [
|
||||
...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 {
|
||||
const normalizedData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return [
|
||||
...data.appointments.map((e) => ({
|
||||
...e,
|
||||
title: `${
|
||||
(e.employee.first_name && e.employee.first_name.substr(0, 1)) || ""
|
||||
} ${e.employee.last_name || ""} OUT`,
|
||||
start: new Date(e.start),
|
||||
end: new Date(e.end)
|
||||
})),
|
||||
...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(),
|
||||
allDay: 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);
|
||||
|
||||
@@ -2,9 +2,14 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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();
|
||||
if (data)
|
||||
return <ScheduleCalendarWrapperComponent events={data} defaultView="day" view={"day"} views={["day"]} date={day} />;
|
||||
else return <div>{t("appointments.labels.nodateselected")}</div>;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
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 { useQuery } from "@apollo/client";
|
||||
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 { 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, {
|
||||
variables: {
|
||||
start: dayjs(day).startOf("day"),
|
||||
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(),
|
||||
variables: queryVariables,
|
||||
skip: !dayjsDay.isValid(),
|
||||
fetchPolicy: "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 (loading) return <LoadingSkeleton paragraph={{ rows: 4 }} />;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
let normalizedData;
|
||||
|
||||
if (data) {
|
||||
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={normalizedData} day={day} />;
|
||||
});
|
||||
|
||||
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 AlertComponent from "../alert/alert.component";
|
||||
import { Timeline } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
|
||||
export default function ScheduleExistingAppointmentsList({ existingAppointments }) {
|
||||
const ScheduleExistingAppointmentsList = React.memo(function ScheduleExistingAppointmentsList({
|
||||
existingAppointments
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (existingAppointments.loading) return <LoadingSpinner />;
|
||||
if (existingAppointments.error) return <AlertComponent message={existingAppointments.error.message} type="error" />;
|
||||
const { loading, error, data } = existingAppointments;
|
||||
|
||||
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 (
|
||||
<div>
|
||||
{t("appointments.labels.priorappointments")}
|
||||
<Timeline
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}))
|
||||
: []
|
||||
}
|
||||
/>
|
||||
<Timeline items={items} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ScheduleExistingAppointmentsList;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -19,12 +19,12 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
calculateScheduleLoad: (endDate) => dispatch(calculateScheduleLoad(endDate))
|
||||
});
|
||||
|
||||
export function ScheduleJobModalComponent({
|
||||
const ScheduleJobModalComponent = React.memo(function ScheduleJobModalComponent({
|
||||
bodyshop,
|
||||
form,
|
||||
existingAppointments,
|
||||
@@ -36,7 +36,7 @@ export function ScheduleJobModalComponent({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [smartOptions, setSmartOptions] = useState([]);
|
||||
|
||||
const handleSmartScheduling = async () => {
|
||||
const handleSmartScheduling = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.post("/scheduling/job", {
|
||||
@@ -48,21 +48,66 @@ export function ScheduleJobModalComponent({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [jobId]);
|
||||
|
||||
const handleDateBlur = () => {
|
||||
const handleDateBlur = useCallback(() => {
|
||||
const values = form.getFieldsValue();
|
||||
|
||||
if (lbrHrsData) {
|
||||
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)
|
||||
form.setFieldsValue({
|
||||
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 (
|
||||
<Row gutter={[16, 16]}>
|
||||
@@ -80,7 +125,6 @@ export function ScheduleJobModalComponent({
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -92,7 +136,6 @@ export function ScheduleJobModalComponent({
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
@@ -107,25 +150,7 @@ export function ScheduleJobModalComponent({
|
||||
<Button onClick={handleSmartScheduling} loading={loading}>
|
||||
{t("appointments.actions.calculate")}
|
||||
</Button>
|
||||
{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>
|
||||
))}
|
||||
{smartOptionsButtons}
|
||||
</Space>
|
||||
</>
|
||||
),
|
||||
@@ -144,20 +169,10 @@ export function ScheduleJobModalComponent({
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item name="color" label={t("appointments.fields.color")}>
|
||||
<Select allowClear>
|
||||
{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>
|
||||
<Select allowClear>{colorOptions}</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name={"alt_transport"} label={t("jobs.fields.alt_transport")}>
|
||||
<Select allowClear>
|
||||
{bodyshop.appt_alt_transport &&
|
||||
bodyshop.appt_alt_transport.map((alt) => <Select.Option key={alt}>{alt}</Select.Option>)}
|
||||
</Select>
|
||||
<Select allowClear>{altTransportOptions}</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name={"note"} label={t("appointments.fields.note")}>
|
||||
<Input />
|
||||
@@ -183,6 +198,6 @@ export function ScheduleJobModalComponent({
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalComponent);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Form, Modal, notification } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -27,13 +27,21 @@ const mapStateToProps = createStructuredSelector({
|
||||
scheduleModal: selectSchedule,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
|
||||
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,
|
||||
bodyshop,
|
||||
toggleModalVisible,
|
||||
@@ -43,168 +51,186 @@ export function ScheduleJobModalContainer({
|
||||
}) {
|
||||
const { open, context, actions } = scheduleModal;
|
||||
const { jobId, job, previousEvent } = context;
|
||||
|
||||
const { refetch } = actions;
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { data: lbrHrsData } = useQuery(QUERY_LBR_HRS_BY_PK, {
|
||||
variables: { id: job && job.id },
|
||||
skip: !job || !job.id,
|
||||
variables: { id: job?.id },
|
||||
skip: !job?.id,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
|
||||
const [insertAppointment] = useMutation(INSERT_APPOINTMENT);
|
||||
const [updateJobStatus] = useMutation(UPDATE_JOBS);
|
||||
|
||||
useEffect(() => {
|
||||
if (job) form.resetFields();
|
||||
}, [job, form]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const existingAppointments = useQuery(QUERY_APPOINTMENTS_BY_JOBID, {
|
||||
variables: { jobid: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !open || !!!jobId
|
||||
skip: !open || !jobId
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
existingAppointments.data &&
|
||||
existingAppointments.data.appointments.length > 0 &&
|
||||
!existingAppointments.data.appointments[0].canceled
|
||||
) {
|
||||
form.setFieldsValue({
|
||||
color: existingAppointments.data.appointments[0].color,
|
||||
if (job) form.resetFields();
|
||||
}, [job, form]);
|
||||
|
||||
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]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("schedule_new_appointment");
|
||||
const handleFinish = useCallback(
|
||||
async (values) => {
|
||||
logImEXEvent("schedule_new_appointment");
|
||||
setLoading(true);
|
||||
|
||||
setLoading(true);
|
||||
if (!!previousEvent) {
|
||||
const cancelAppt = await cancelAppointment({
|
||||
variables: { appid: previousEvent }
|
||||
});
|
||||
|
||||
if (!!cancelAppt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.canceling", {
|
||||
message: JSON.stringify(cancelAppt.errors)
|
||||
})
|
||||
if (previousEvent) {
|
||||
const cancelAppt = await cancelAppointment({
|
||||
variables: { appid: previousEvent }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
notification["success"]({
|
||||
message: t("appointments.successes.canceled")
|
||||
});
|
||||
}
|
||||
|
||||
if (existingAppointments.data.appointments.length > 0) {
|
||||
await Promise.all(
|
||||
existingAppointments.data.appointments.map((app) => {
|
||||
return cancelAppointment({
|
||||
variables: { appid: app.id }
|
||||
if (cancelAppt.errors) {
|
||||
notification.error({
|
||||
message: t("appointments.errors.canceling", {
|
||||
message: JSON.stringify(cancelAppt.errors)
|
||||
})
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const appt = await insertAppointment({
|
||||
variables: {
|
||||
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
|
||||
notification.success({
|
||||
message: t("appointments.successes.canceled")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!appt.errors) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.appointmentinsert(DateTimeFormat(values.start)),
|
||||
type: "appointmentinsert"
|
||||
});
|
||||
}
|
||||
const existingApps = existingAppointments.data?.appointments || [];
|
||||
if (existingApps.length > 0) {
|
||||
await Promise.all(
|
||||
existingApps.map((app) =>
|
||||
cancelAppointment({
|
||||
variables: { appid: app.id }
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!!appt.errors) {
|
||||
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({
|
||||
const appt = await insertAppointment({
|
||||
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
|
||||
}
|
||||
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 (!!jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
if (!appt.errors) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.appointmentinsert(DateTimeFormat(values.start)),
|
||||
type: "appointmentinsert"
|
||||
});
|
||||
} else {
|
||||
notification.error({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
message: JSON.stringify(appt.errors)
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
toggleModalVisible();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
notification.success({
|
||||
message: t("appointments.successes.created")
|
||||
});
|
||||
}
|
||||
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 (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
onCancel={toggleModalVisible}
|
||||
onOk={() => form.submit()}
|
||||
width={"90%"}
|
||||
width="90%"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
okButtonProps={{
|
||||
@@ -217,10 +243,9 @@ export function ScheduleJobModalContainer({
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
initialValues={{
|
||||
notifyCustomer: !!(job && job.ownr_ea),
|
||||
email: (job && job.ownr_ea) || "",
|
||||
notifyCustomer: !!job?.ownr_ea,
|
||||
email: job?.ownr_ea || "",
|
||||
start: null,
|
||||
// smartDates: [],
|
||||
scheduled_completion: null,
|
||||
color: context.color,
|
||||
alt_transport: context.alt_transport,
|
||||
@@ -236,6 +261,6 @@ export function ScheduleJobModalContainer({
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalContainer);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Form, Input, Popover, Select, Space } from "antd";
|
||||
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 { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -13,142 +13,143 @@ import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
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 [insertAppointment] = useMutation(INSERT_MANUAL_APPT);
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||
const [insertAppointment] = useMutation(INSERT_MANUAL_APPT, {
|
||||
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
||||
});
|
||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT, {
|
||||
refetchQueries: ["QUERY_ALL_ACTIVE_APPOINTMENTS"]
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
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(() => {
|
||||
if (visibility && event) {
|
||||
form.setFieldsValue(event);
|
||||
} else if (!visibility) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visibility, form, event]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("schedule_manual_event");
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (event && event.id) {
|
||||
updateAppointment({
|
||||
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 colorOptions = useMemo(() => {
|
||||
return bodyshop.appt_colors.map((col, idx) => (
|
||||
<Select.Option key={idx} value={col.color.hex}>
|
||||
{col.label}
|
||||
</Select.Option>
|
||||
));
|
||||
}, [bodyshop.appt_colors]);
|
||||
|
||||
const overlay = (
|
||||
<Card>
|
||||
<div>
|
||||
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.title")}
|
||||
name="title"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.note")} name="note">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.start")}
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.end")}
|
||||
name="end"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
async validator(rule, value) {
|
||||
if (value) {
|
||||
const { start } = form.getFieldsValue();
|
||||
if (dayjs(start).isAfter(dayjs(value))) {
|
||||
return Promise.reject(t("employees.labels.endmustbeafterstart"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.title")}
|
||||
name="title"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.note")} name="note">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.start")}
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("appointments.fields.end")}
|
||||
name="end"
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (value) {
|
||||
const start = form.getFieldValue("start");
|
||||
if (dayjs(start).isAfter(dayjs(value))) {
|
||||
return Promise.reject(t("employees.labels.endmustbeafterstart"));
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.color")} name="color">
|
||||
<Select>
|
||||
{bodyshop.appt_colors.map((col, idx) => (
|
||||
<Select.Option key={idx} value={col.color.hex}>
|
||||
{col.label}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>{t("general.actions.cancel")}</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<FormDateTimePickerComponent />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("appointments.fields.color")} name="color">
|
||||
<Select>{colorOptions}</Select>
|
||||
</Form.Item>
|
||||
<Space wrap>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>{t("general.actions.cancel")}</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const handleClick = (e) => {
|
||||
setVisibility(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover content={overlay} open={visibility}>
|
||||
<Button loading={loading} onClick={handleClick}>
|
||||
<Button onClick={handleClick}>
|
||||
{event ? t("appointments.actions.reschedule") : t("appointments.labels.manualevent")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(ScheduleManualEvent);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Popover } from "antd";
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function ScheduleProductionList() {
|
||||
const ScheduleProductionList = React.memo(function ScheduleProductionList() {
|
||||
const { t } = useTranslation();
|
||||
const [callQuery, { loading, error, data }] = useLazyQuery(QUERY_JOBS_IN_PRODUCTION);
|
||||
|
||||
const content = () => {
|
||||
const content = useCallback(() => {
|
||||
return (
|
||||
<Card>
|
||||
<div onClick={(e) => e.stopPropagation()} className="jobs-in-production-table">
|
||||
{loading ? <LoadingSkeleton /> : null}
|
||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||
{data ? (
|
||||
{loading && <LoadingSkeleton />}
|
||||
{error && <AlertComponent message={error.message} type="error" />}
|
||||
{data && data.jobs && (
|
||||
<table>
|
||||
<tbody>
|
||||
{data && data.jobs
|
||||
? data.jobs.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>{`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}</td>
|
||||
<td>{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${
|
||||
j.larhrs.aggregate.sum.mod_lb_hrs || "0"
|
||||
}`}</td>
|
||||
<td>
|
||||
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: null}
|
||||
{data.jobs.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>{`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}</td>
|
||||
<td>{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${
|
||||
j.larhrs.aggregate.sum.mod_lb_hrs || "0"
|
||||
}`}</td>
|
||||
<td>
|
||||
<DateTimeFormatter>{j.scheduled_completion}</DateTimeFormatter>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}, [loading, error, data]);
|
||||
|
||||
return (
|
||||
<Popover content={content} trigger="click" placement="bottomRight">
|
||||
<Button onClick={() => callQuery()}>
|
||||
<Button onClick={callQuery}>
|
||||
{t("appointments.labels.inproduction")}
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ScheduleProductionList;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { Button } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
|
||||
@@ -10,49 +10,46 @@ import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
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 client = useApolloClient();
|
||||
const handleVerify = async () => {
|
||||
|
||||
const handleVerify = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const {
|
||||
data: { arrJobs, compJobs, prodJobs }
|
||||
} = await client.query({
|
||||
query: QUERY_SCHEDULE_LOAD_DATA,
|
||||
variables: { start: dayjs(), end: dayjs().add(180, "day") }
|
||||
});
|
||||
try {
|
||||
const {
|
||||
data: { arrJobs, compJobs, prodJobs }
|
||||
} = await client.query({
|
||||
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.
|
||||
const issues = [];
|
||||
// Check that the completing jobs are either in production or arriving within the next 180 days.
|
||||
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) => {
|
||||
const inProdJobs = prodJobs.find((p) => p.id === j.id);
|
||||
const inArrJobs = arrJobs.find((p) => p.id === j.id);
|
||||
console.log("The following completing jobs are not in production or arriving within the next 180 days:", issues);
|
||||
} catch (error) {
|
||||
console.error("Error verifying schedule integrity:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
if (!(inProdJobs || inArrJobs)) {
|
||||
// NOT FOUND!
|
||||
issues.push(j);
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
"The following completing jobs are not in production, or are arriving within the next 180 days. ",
|
||||
issues
|
||||
);
|
||||
// TODO: A Global helper with developer emails
|
||||
if (currentUser.email !== "patrick@imex.prod") {
|
||||
return null;
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
return (
|
||||
<Button loading={loading} onClick={handleVerify}>
|
||||
Developer Use Only - Verify Schedule Integrity
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
if (currentUser.email === "patrick@imex.prod")
|
||||
return (
|
||||
<Button loading={loading} onClick={handleVerify}>
|
||||
Developer Use Only - Verify Schedule Integrity
|
||||
</Button>
|
||||
);
|
||||
else return null;
|
||||
}
|
||||
export default connect(mapStateToProps)(ScheduleVerifyIntegrity);
|
||||
|
||||
Reference in New Issue
Block a user