From 10ba19f0d2eea4278edd49c480d9447f53ec6686 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 16 Sep 2024 23:02:20 -0400 Subject: [PATCH] IO-2932-Scheduling-Lag-on-AIO: Full Optimization of all Schedule related components. Signed-off-by: Dave Richer --- .../schedule-ats-summary.component.jsx | 38 +-- .../schedule-block-day.component.jsx | 82 +++--- ...hedule-calendar-header-graph.component.jsx | 81 +++--- .../schedule-calendar-header.component.jsx | 244 +++++++--------- .../schedule-calendar-util.js | 49 ++-- .../scheduler-calendar-wrapper.component.jsx | 108 ++++--- .../schedule-calendar.component.jsx | 138 +++++---- .../schedule-calendar.container.jsx | 61 ++-- .../schedule-day-view.component.jsx | 15 +- .../schedule-day-view.container.jsx | 82 +++--- ...e-existing-appointments-list.component.jsx | 55 ++-- .../schedule-job-modal.component.jsx | 99 ++++--- .../schedule-job-modal.container.jsx | 269 ++++++++++-------- .../schedule-manual-event.component.jsx | 225 +++++++-------- .../schedule-production-list.component.jsx | 58 ++-- .../schedule-verify-integrity.component.jsx | 73 +++-- 16 files changed, 886 insertions(+), 791 deletions(-) diff --git a/client/src/components/schedule-ats-summary/schedule-ats-summary.component.jsx b/client/src/components/schedule-ats-summary/schedule-ats-summary.component.jsx index 5e7619495..29b926e12 100644 --- a/client/src/components/schedule-ats-summary/schedule-ats-summary.component.jsx +++ b/client/src/components/schedule-ats-summary/schedule-ats-summary.component.jsx @@ -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 ( {t("schedule.labels.atssummary")} - {Object.keys(atsSummary).map((key) => ( - {`${key}: ${atsSummary[key]}`} + {Object.entries(atsSummary).map(([key, value]) => ( + {`${key}: ${value}`} ))} ); + } return null; -} +}); -export default connect(mapStateToProps, mapDispatchToProps)(ScheduleAtsSummary); +export default ScheduleAtsSummary; diff --git a/client/src/components/schedule-block-day/schedule-block-day.component.jsx b/client/src/components/schedule-block-day/schedule-block-day.component.jsx index e579697db..646ad91f5 100644 --- a/client/src/components/schedule-block-day/schedule-block-day.component.jsx +++ b/client/src/components/schedule-block-day/schedule-block-day.component.jsx @@ -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 ( {children} ); -} +}); -export default connect(mapStateToProps, mapDispatchToProps)(ScheduleBlockDay); +export default connect(mapStateToProps)(ScheduleBlockDay); diff --git a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header-graph.component.jsx b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header-graph.component.jsx index dd23c669f..c6ddf9e36 100644 --- a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header-graph.component.jsx +++ b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header-graph.component.jsx @@ -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 = ( -
- - {t("appointments.labels.expectedprodhrs")} - {loadData?.expectedHours?.toFixed(1)} - {t("appointments.labels.expectedjobs")} - {loadData?.expectedJobCount} - - - - - - - - - - -
+ const popContent = useMemo( + () => ( +
+ + {t("appointments.labels.expectedprodhrs")} + {loadData?.expectedHours?.toFixed(1) || 0} + {t("appointments.labels.expectedjobs")} + {loadData?.expectedJobCount || 0} + + + + + + + + + + +
+ ), + [t, loadData, data] ); return ( @@ -66,6 +59,6 @@ export function ScheduleCalendarHeaderGraph({ bodyshop, loadData }) { ); -} +}); export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderGraph); diff --git a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx index 9a83f7cae..4d317cc67 100644 --- a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx +++ b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx @@ -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 = () => ( -
e.stopPropagation()}> - - - {loadData && loadData.allJobsOut ? ( - loadData.allJobsOut.map((j) => ( - - - - - + const jobsOutPopup = useCallback( + () => ( +
e.stopPropagation()}> +
- {j.ro_number} ( - {j.status}) - - - - {`(${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")})`} - - - {j.scheduled_completion} - -
+ + {loadData && loadData.allJobsOut && loadData.allJobsOut.length > 0 ? ( + loadData.allJobsOut.map((j) => ( + + + + + + + )) + ) : ( + + - )) - ) : ( - - - - )} - -
+ {j.ro_number} ({j.status}) + + + + {`(${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")})`} + + {j.scheduled_completion} +
{t("appointments.labels.nocompletingjobs")}
- {t("appointments.labels.nocompletingjobs")} -
-
+ )} + + + + ), + [loadData, t] ); - const jobsInPopup = () => ( -
e.stopPropagation()}> - - - {loadData && loadData.allJobsIn ? ( - loadData.allJobsIn.map((j) => ( - - - - - + const jobsInPopup = useCallback( + () => ( +
e.stopPropagation()}> +
- {j.ro_number} - - - - {`(${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")})`} - - {j.scheduled_in} -
+ + {loadData && loadData.allJobsIn && loadData.allJobsIn.length > 0 ? ( + loadData.allJobsIn.map((j) => ( + + + + + + + )) + ) : ( + + - )) - ) : ( - - - - )} - -
+ {j.ro_number} + + + + {`(${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")})`} + + {j.scheduled_in} +
{t("appointments.labels.noarrivingjobs")}
- {t("appointments.labels.noarrivingjobs")} -
-
+ )} + + + + ), + [loadData, t] ); - const LoadComponent = loadData ? ( -
+ const LoadComponent = useMemo(() => { + if (!loadData) return null; + return ( +
- {(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)} - {(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(1)} + {(loadData.allHoursOut || 0).toFixed(1)} - -
-
    - {Object.keys(ATSToday).map((key, idx) => ( -
  • {`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}
  • - ))} -
+
+
    + {Object.keys(ATSToday).map((key, idx) => ( +
  • {`${key === "null" || key === "undefined" ? "N/A" : key}: ${ATSToday[key].length}`}
  • + ))} +
+
-
- ) : 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 (
0} date={date} refetch={refetch}> -
+
{label} {InstanceRenderMgr({ imex: calculating ? : LoadComponent, @@ -216,6 +184,6 @@ export function ScheduleCalendarHeaderComponent({
); -} +}); export default connect(mapStateToProps, mapDispatchToProps)(ScheduleCalendarHeaderComponent); diff --git a/client/src/components/schedule-calendar-wrapper/schedule-calendar-util.js b/client/src/components/schedule-calendar-wrapper/schedule-calendar-util.js index 554c837db..234b73495 100644 --- a/client/src/components/schedule-calendar-wrapper/schedule-calendar-util.js +++ b/client/src/components/schedule-calendar-wrapper/schedule-calendar-util.js @@ -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); } diff --git a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx index fc79cd4a9..a261daf6a 100644 --- a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx +++ b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx @@ -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) => , + [bodyshop, refetch] + ); + + const headerComponent = useCallback( + (p) => , + [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) => + event: eventComponent, + header: headerComponent }} {...otherProps} /> ); -} +}); export default connect(mapStateToProps, null)(ScheduleCalendarWrapperComponent); diff --git a/client/src/components/schedule-calendar/schedule-calendar.component.jsx b/client/src/components/schedule-calendar/schedule-calendar.component.jsx index 476094dd8..7382dc08f 100644 --- a/client/src/components/schedule-calendar/schedule-calendar.component.jsx +++ b/client/src/components/schedule-calendar/schedule-calendar.component.jsx @@ -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 ( @@ -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} /> - {bodyshop.appt_colors && - bodyshop.appt_colors.map((color) => ( - - {color.label} - - ))} - + - + @@ -183,6 +198,6 @@ export function ScheduleJobModalComponent({ ); -} +}); export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalComponent); diff --git a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx index eac29d274..c22d43f3f 100644 --- a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx +++ b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx @@ -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 ( 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({ ); -} +}); export default connect(mapStateToProps, mapDispatchToProps)(ScheduleJobModalContainer); diff --git a/client/src/components/schedule-manual-event/schedule-manual-event.component.jsx b/client/src/components/schedule-manual-event/schedule-manual-event.component.jsx index e6bb2896d..b795015b8 100644 --- a/client/src/components/schedule-manual-event/schedule-manual-event.component.jsx +++ b/client/src/components/schedule-manual-event/schedule-manual-event.component.jsx @@ -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) => ( + + {col.label} + + )); + }, [bodyshop.appt_colors]); const overlay = ( -
-
- - - - - - - - - - ({ - 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(); - } + + + + + + + + + + + ({ + 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(); } - }) - ]} - > - - - - - - - - - - - -
+ } + }) + ]} + > + + + + + + + + + +
); - const handleClick = (e) => { - setVisibility(true); - }; - return ( - ); -} +}); + +export default connect(mapStateToProps)(ScheduleManualEvent); diff --git a/client/src/components/schedule-production-list/schedule-production-list.component.jsx b/client/src/components/schedule-production-list/schedule-production-list.component.jsx index b4e5dcf31..a91b6122f 100644 --- a/client/src/components/schedule-production-list/schedule-production-list.component.jsx +++ b/client/src/components/schedule-production-list/schedule-production-list.component.jsx @@ -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 (
e.stopPropagation()} className="jobs-in-production-table"> - {loading ? : null} - {error ? : null} - {data ? ( + {loading && } + {error && } + {data && data.jobs && ( - {data && data.jobs - ? data.jobs.map((j) => ( - - - - - - - - )) - : null} + {data.jobs.map((j) => ( + + + + + + + + ))}
- {j.ro_number} - - - {`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${ - j.larhrs.aggregate.sum.mod_lb_hrs || "0" - }`} - {j.scheduled_completion} -
+ {j.ro_number} + + + {`${j.v_model_yr || ""} ${j.v_make_desc || ""} ${j.v_model_desc || ""}`}{`${j.labhrs.aggregate.sum.mod_lb_hrs || "0"} / ${ + j.larhrs.aggregate.sum.mod_lb_hrs || "0" + }`} + {j.scheduled_completion} +
- ) : null} + )}
); - }; + }, [loading, error, data]); return ( - ); -} +}); + +export default ScheduleProductionList; diff --git a/client/src/components/schedule-verify-integrity/schedule-verify-integrity.component.jsx b/client/src/components/schedule-verify-integrity/schedule-verify-integrity.component.jsx index 83693df19..a1fd85dab 100644 --- a/client/src/components/schedule-verify-integrity/schedule-verify-integrity.component.jsx +++ b/client/src/components/schedule-verify-integrity/schedule-verify-integrity.component.jsx @@ -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 ( + + ); +}); - if (currentUser.email === "patrick@imex.prod") - return ( - - ); - else return null; -} +export default connect(mapStateToProps)(ScheduleVerifyIntegrity);