From eeed004fe246dd7b543b8c48f26070a43dc2d842 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 18 Sep 2024 15:00:32 -0400 Subject: [PATCH] IO-2932-Scheduling-Lag-on-AIO-HotFix - Remove timezone from DayJS for scheduling by adjusting the localizer Signed-off-by: Dave Richer --- client/package-lock.json | 1 + client/package.json | 1 + .../schedule-calendar-wrapper/localizer.js | 505 ++++++++++++++++++ .../scheduler-calendar-wrapper.component.jsx | 5 +- 4 files changed, 510 insertions(+), 2 deletions(-) create mode 100644 client/src/components/schedule-calendar-wrapper/localizer.js diff --git a/client/package-lock.json b/client/package-lock.json index f794877d6..04b6d5678 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "bodyshop", "version": "0.2.1", + "hasInstallScript": true, "dependencies": { "@ant-design/pro-layout": "^7.19.12", "@apollo/client": "^3.11.4", diff --git a/client/package.json b/client/package.json index e89aabfb0..3561564f6 100644 --- a/client/package.json +++ b/client/package.json @@ -84,6 +84,7 @@ "web-vitals": "^3.5.2" }, "scripts": { + "postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'", "analyze": "source-map-explorer 'build/static/js/*.js'", "start": "vite", "build": "dotenvx run --env-file=.env.development.imex -- vite build", diff --git a/client/src/components/schedule-calendar-wrapper/localizer.js b/client/src/components/schedule-calendar-wrapper/localizer.js new file mode 100644 index 000000000..e91016416 --- /dev/null +++ b/client/src/components/schedule-calendar-wrapper/localizer.js @@ -0,0 +1,505 @@ +import isBetween from "dayjs/plugin/isBetween"; +import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; +import isSameOrBefore from "dayjs/plugin/isSameOrBefore"; +import localeData from "dayjs/plugin/localeData"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +import minMax from "dayjs/plugin/minMax"; +import utc from "dayjs/plugin/utc"; +import { DateLocalizer } from "react-big-calendar"; + +function arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; +} + +function iterableToArrayLimit(arr, i) { + if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + return _arr; +} + +function unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen); +} + +function arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + for (var i = 0, arr2 = new Array(len); i < len; i++) { + arr2[i] = arr[i]; + } + return arr2; +} + +function nonIterableRest() { + throw new TypeError( + "Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method." + ); +} + +function _slicedToArray(arr, i) { + return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest(); +} + +function fixUnit(unit) { + var datePart = unit ? unit.toLowerCase() : unit; + if (datePart === "FullYear") { + datePart = "year"; + } else if (!datePart) { + datePart = undefined; + } + return datePart; +} + +var timeRangeFormat = function timeRangeFormat(_ref3, culture, local) { + var start = _ref3.start, + end = _ref3.end; + return local.format(start, "LT", culture) + " – " + local.format(end, "LT", culture); +}; +var timeRangeStartFormat = function timeRangeStartFormat(_ref4, culture, local) { + var start = _ref4.start; + return local.format(start, "LT", culture) + " – "; +}; +var timeRangeEndFormat = function timeRangeEndFormat(_ref5, culture, local) { + var end = _ref5.end; + return " – " + local.format(end, "LT", culture); +}; +var weekRangeFormat = function weekRangeFormat(_ref, culture, local) { + var start = _ref.start, + end = _ref.end; + return ( + local.format(start, "MMMM DD", culture) + + " – " + + // updated to use this localizer 'eq()' method + local.format(end, local.eq(start, end, "month") ? "DD" : "MMMM DD", culture) + ); +}; +var dateRangeFormat = function dateRangeFormat(_ref2, culture, local) { + var start = _ref2.start, + end = _ref2.end; + return local.format(start, "L", culture) + " – " + local.format(end, "L", culture); +}; + +var formats = { + dateFormat: "DD", + dayFormat: "DD ddd", + weekdayFormat: "ddd", + selectRangeFormat: timeRangeFormat, + eventTimeRangeFormat: timeRangeFormat, + eventTimeRangeStartFormat: timeRangeStartFormat, + eventTimeRangeEndFormat: timeRangeEndFormat, + timeGutterFormat: "LT", + monthHeaderFormat: "MMMM YYYY", + dayHeaderFormat: "dddd MMM DD", + dayRangeHeaderFormat: weekRangeFormat, + agendaHeaderFormat: dateRangeFormat, + agendaDateFormat: "ddd MMM DD", + agendaTimeFormat: "LT", + agendaTimeRangeFormat: timeRangeFormat +}; + +const localizer = (dayjsLib) => { + // load dayjs plugins + dayjsLib.extend(isBetween); + dayjsLib.extend(isSameOrAfter); + dayjsLib.extend(isSameOrBefore); + dayjsLib.extend(localeData); + dayjsLib.extend(localizedFormat); + dayjsLib.extend(minMax); + dayjsLib.extend(utc); + var locale = function locale(dj, c) { + return c ? dj.locale(c) : dj; + }; + + // if the timezone plugin is loaded, + // then use the timezone aware version + + //TODO This was the issue entirely... + // var dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib; + var dayjs = dayjsLib; + + function getTimezoneOffset(date) { + // ensures this gets cast to timezone + return dayjs(date).toDate().getTimezoneOffset(); + } + + function getDstOffset(start, end) { + var _st$tz$$x$$timezone; + // convert to dayjs, in case + var st = dayjs(start); + var ed = dayjs(end); + // if not using the dayjs timezone plugin + if (!dayjs.tz) { + return st.toDate().getTimezoneOffset() - ed.toDate().getTimezoneOffset(); + } + /** + * If a default timezone has been applied, then + * use this to get the proper timezone offset, otherwise default + * the timezone to the browser local + */ + var tzName = + (_st$tz$$x$$timezone = st.tz().$x.$timezone) !== null && _st$tz$$x$$timezone !== void 0 + ? _st$tz$$x$$timezone + : dayjsLib.tz.guess(); + // invert offsets to be inline with moment.js + var startOffset = -dayjs.tz(+st, tzName).utcOffset(); + var endOffset = -dayjs.tz(+ed, tzName).utcOffset(); + return startOffset - endOffset; + } + + function getDayStartDstOffset(start) { + var dayStart = dayjs(start).startOf("day"); + return getDstOffset(dayStart, start); + } + + /*** BEGIN localized date arithmetic methods with dayjs ***/ + function defineComparators(a, b, unit) { + var datePart = fixUnit(unit); + var dtA = datePart ? dayjs(a).startOf(datePart) : dayjs(a); + var dtB = datePart ? dayjs(b).startOf(datePart) : dayjs(b); + return [dtA, dtB, datePart]; + } + + function startOf() { + var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var unit = arguments.length > 1 ? arguments[1] : undefined; + var datePart = fixUnit(unit); + if (datePart) { + return dayjs(date).startOf(datePart).toDate(); + } + return dayjs(date).toDate(); + } + + function endOf() { + var date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var unit = arguments.length > 1 ? arguments[1] : undefined; + var datePart = fixUnit(unit); + if (datePart) { + return dayjs(date).endOf(datePart).toDate(); + } + return dayjs(date).toDate(); + } + + // dayjs comparison operations *always* convert both sides to dayjs objects + // prior to running the comparisons + function eq(a, b, unit) { + var _defineComparators = defineComparators(a, b, unit), + _defineComparators2 = _slicedToArray(_defineComparators, 3), + dtA = _defineComparators2[0], + dtB = _defineComparators2[1], + datePart = _defineComparators2[2]; + return dtA.isSame(dtB, datePart); + } + + function neq(a, b, unit) { + return !eq(a, b, unit); + } + + function gt(a, b, unit) { + var _defineComparators3 = defineComparators(a, b, unit), + _defineComparators4 = _slicedToArray(_defineComparators3, 3), + dtA = _defineComparators4[0], + dtB = _defineComparators4[1], + datePart = _defineComparators4[2]; + return dtA.isAfter(dtB, datePart); + } + + function lt(a, b, unit) { + var _defineComparators5 = defineComparators(a, b, unit), + _defineComparators6 = _slicedToArray(_defineComparators5, 3), + dtA = _defineComparators6[0], + dtB = _defineComparators6[1], + datePart = _defineComparators6[2]; + return dtA.isBefore(dtB, datePart); + } + + function gte(a, b, unit) { + var _defineComparators7 = defineComparators(a, b, unit), + _defineComparators8 = _slicedToArray(_defineComparators7, 3), + dtA = _defineComparators8[0], + dtB = _defineComparators8[1], + datePart = _defineComparators8[2]; + return dtA.isSameOrBefore(dtB, datePart); + } + + function lte(a, b, unit) { + var _defineComparators9 = defineComparators(a, b, unit), + _defineComparators10 = _slicedToArray(_defineComparators9, 3), + dtA = _defineComparators10[0], + dtB = _defineComparators10[1], + datePart = _defineComparators10[2]; + return dtA.isSameOrBefore(dtB, datePart); + } + + function inRange(day, min, max) { + var unit = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "day"; + var datePart = fixUnit(unit); + var djDay = dayjs(day); + var djMin = dayjs(min); + var djMax = dayjs(max); + return djDay.isBetween(djMin, djMax, datePart, "[]"); + } + + function min(dateA, dateB) { + var dtA = dayjs(dateA); + var dtB = dayjs(dateB); + var minDt = dayjsLib.min(dtA, dtB); + return minDt.toDate(); + } + + function max(dateA, dateB) { + var dtA = dayjs(dateA); + var dtB = dayjs(dateB); + var maxDt = dayjsLib.max(dtA, dtB); + return maxDt.toDate(); + } + + function merge(date, time) { + if (!date && !time) return null; + var tm = dayjs(time).format("HH:mm:ss"); + var dt = dayjs(date).startOf("day").format("MM/DD/YYYY"); + // We do it this way to avoid issues when timezone switching + return dayjsLib("".concat(dt, " ").concat(tm), "MM/DD/YYYY HH:mm:ss").toDate(); + } + + function add(date, adder, unit) { + var datePart = fixUnit(unit); + return dayjs(date).add(adder, datePart).toDate(); + } + + function range(start, end) { + var unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "day"; + var datePart = fixUnit(unit); + // because the add method will put these in tz, we have to start that way + var current = dayjs(start).toDate(); + var days = []; + while (lte(current, end)) { + days.push(current); + current = add(current, 1, datePart); + } + return days; + } + + function ceil(date, unit) { + var datePart = fixUnit(unit); + var floor = startOf(date, datePart); + return eq(floor, date) ? floor : add(floor, 1, datePart); + } + + function diff(a, b) { + var unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "day"; + var datePart = fixUnit(unit); + // don't use 'defineComparators' here, as we don't want to mutate the values + var dtA = dayjs(a); + var dtB = dayjs(b); + return dtB.diff(dtA, datePart); + } + + function minutes(date) { + var dt = dayjs(date); + return dt.minutes(); + } + + function firstOfWeek(culture) { + var data = culture ? dayjsLib.localeData(culture) : dayjsLib.localeData(); + return data ? data.firstDayOfWeek() : 0; + } + + function firstVisibleDay(date) { + return dayjs(date).startOf("month").startOf("week").toDate(); + } + + function lastVisibleDay(date) { + return dayjs(date).endOf("month").endOf("week").toDate(); + } + + function visibleDays(date) { + var current = firstVisibleDay(date); + var last = lastVisibleDay(date); + var days = []; + while (lte(current, last)) { + days.push(current); + current = add(current, 1, "d"); + } + return days; + } + + /*** END localized date arithmetic methods with dayjs ***/ + + /** + * Moved from TimeSlots.js, this method overrides the method of the same name + * in the localizer.js, using dayjs to construct the js Date + * @param {Date} dt - date to start with + * @param {Number} minutesFromMidnight + * @param {Number} offset + * @returns {Date} + */ + function getSlotDate(dt, minutesFromMidnight, offset) { + return dayjs(dt) + .startOf("day") + .minute(minutesFromMidnight + offset) + .toDate(); + } + + // dayjs will automatically handle DST differences in it's calculations + function getTotalMin(start, end) { + return diff(start, end, "minutes"); + } + + function getMinutesFromMidnight(start) { + var dayStart = dayjs(start).startOf("day"); + var day = dayjs(start); + return day.diff(dayStart, "minutes") + getDayStartDstOffset(start); + } + + // These two are used by DateSlotMetrics + function continuesPrior(start, first) { + var djStart = dayjs(start); + var djFirst = dayjs(first); + return djStart.isBefore(djFirst, "day"); + } + + function continuesAfter(start, end, last) { + var djEnd = dayjs(end); + var djLast = dayjs(last); + return djEnd.isSameOrAfter(djLast, "minutes"); + } + + function daySpan(start, end) { + var startDay = dayjs(start); + var endDay = dayjs(end); + return endDay.diff(startDay, "day"); + } + + // These two are used by eventLevels + function sortEvents(_ref6) { + var _ref6$evtA = _ref6.evtA, + aStart = _ref6$evtA.start, + aEnd = _ref6$evtA.end, + aAllDay = _ref6$evtA.allDay, + _ref6$evtB = _ref6.evtB, + bStart = _ref6$evtB.start, + bEnd = _ref6$evtB.end, + bAllDay = _ref6$evtB.allDay; + var startSort = +startOf(aStart, "day") - +startOf(bStart, "day"); + var durA = daySpan(aStart, aEnd); + var durB = daySpan(bStart, bEnd); + return ( + startSort || + // sort by start Day first + durB - durA || + // events spanning multiple days go first + !!bAllDay - !!aAllDay || + // then allDay single day events + +aStart - +bStart || + // then sort by start time *don't need dayjs conversion here + +aEnd - +bEnd // then sort by end time *don't need dayjs conversion here either + ); + } + + function inEventRange(_ref7) { + var _ref7$event = _ref7.event, + start = _ref7$event.start, + end = _ref7$event.end, + _ref7$range = _ref7.range, + rangeStart = _ref7$range.start, + rangeEnd = _ref7$range.end; + var startOfDay = dayjs(start).startOf("day"); + var eEnd = dayjs(end); + var rStart = dayjs(rangeStart); + var rEnd = dayjs(rangeEnd); + var startsBeforeEnd = startOfDay.isSameOrBefore(rEnd, "day"); + // when the event is zero duration we need to handle a bit differently + var sameMin = !startOfDay.isSame(eEnd, "minutes"); + var endsAfterStart = sameMin ? eEnd.isAfter(rStart, "minutes") : eEnd.isSameOrAfter(rStart, "minutes"); + return startsBeforeEnd && endsAfterStart; + } + + function isSameDate(date1, date2) { + var dt = dayjs(date1); + var dt2 = dayjs(date2); + return dt.isSame(dt2, "day"); + } + + /** + * This method, called once in the localizer constructor, is used by eventLevels + * 'eventSegments()' to assist in determining the 'span' of the event in the display, + * specifically when using a timezone that is greater than the browser native timezone. + * @returns number + */ + function browserTZOffset() { + /** + * Date.prototype.getTimezoneOffset horrifically flips the positive/negative from + * what you see in it's string, so we have to jump through some hoops to get a value + * we can actually compare. + */ + var dt = new Date(); + var neg = /-/.test(dt.toString()) ? "-" : ""; + var dtOffset = dt.getTimezoneOffset(); + var comparator = Number("".concat(neg).concat(Math.abs(dtOffset))); + // dayjs correctly provides positive/negative offset, as expected + var mtOffset = dayjs().utcOffset(); + return mtOffset > comparator ? 1 : 0; + } + + return new DateLocalizer({ + formats: formats, + firstOfWeek: firstOfWeek, + firstVisibleDay: firstVisibleDay, + lastVisibleDay: lastVisibleDay, + visibleDays: visibleDays, + format: function format(value, _format, culture) { + return locale(dayjs(value), culture).format(_format); + }, + lt: lt, + lte: lte, + gt: gt, + gte: gte, + eq: eq, + neq: neq, + merge: merge, + inRange: inRange, + startOf: startOf, + endOf: endOf, + range: range, + add: add, + diff: diff, + ceil: ceil, + min: min, + max: max, + minutes: minutes, + getSlotDate: getSlotDate, + getTimezoneOffset: getTimezoneOffset, + getDstOffset: getDstOffset, + getTotalMin: getTotalMin, + getMinutesFromMidnight: getMinutesFromMidnight, + continuesPrior: continuesPrior, + continuesAfter: continuesAfter, + sortEvents: sortEvents, + inEventRange: inEventRange, + isSameDate: isSameDate, + browserTZOffset: browserTZOffset + }); +}; +export default localizer; 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..fb866d89d 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,7 +1,7 @@ import dayjs from "../../utils/day"; import queryString from "query-string"; import React from "react"; -import { Calendar, dayjsLocalizer } from "react-big-calendar"; +import { Calendar } from "react-big-calendar"; import { connect } from "react-redux"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; @@ -14,12 +14,13 @@ import { selectProblemJobs } from "../../redux/application/application.selectors import { Alert, Collapse, Space } from "antd"; import { Trans, useTranslation } from "react-i18next"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import local from "./localizer"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, problemJobs: selectProblemJobs }); -const localizer = dayjsLocalizer(dayjs); +const localizer = local(dayjs); export function ScheduleCalendarWrapperComponent({ bodyshop,