From 30cb4ef5620b43a389ac8aa90cafb545062121bf Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 16 Sep 2024 11:06:56 -0700 Subject: [PATCH 01/13] IO-2934 Active Jobs Estimator Filter/Sorter Signed-off-by: Allan Carr --- client/src/components/jobs-list/jobs-list.component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index 004addbad..f7164f748 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -250,8 +250,8 @@ export function JobsList({ bodyshop }) { }, { title: t("jobs.labels.estimator"), - dataIndex: "jobs.labels.estimator", - key: "jobs.labels.estimator", + dataIndex: "estimator", + key: "estimator", ellipsis: true, responsive: ["xl"], sorter: (a, b) => From b39a5b755e848be512d48e7efbe6bb0d9cb485d0 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 16 Sep 2024 12:07:35 -0700 Subject: [PATCH 02/13] IO-2920 Add hasura changes for cash discount & add config page. --- bodyshop_translations.babel | 175 +- .../shop-info/shop-info.component.jsx | 12 + .../shop-intellipay-config.component.jsx | 54 + client/src/graphql/bodyshop.queries.js | 6 +- client/src/pages/shop/shop.page.component.jsx | 18 +- client/src/translations/en_us/common.json | 7160 +++++++++-------- client/src/translations/es/common.json | 7160 +++++++++-------- client/src/translations/fr/common.json | 7160 +++++++++-------- hasura/metadata/tables.yaml | 59 + .../down.sql | 4 + .../up.sql | 2 + 11 files changed, 11067 insertions(+), 10743 deletions(-) create mode 100644 client/src/components/shop-info/shop-intellipay-config.component.jsx create mode 100644 hasura/migrations/1726511165929_alter_table_public_bodyshops_add_column_intellipay_config/down.sql create mode 100644 hasura/migrations/1726511165929_alter_table_public_bodyshops_add_column_intellipay_config/up.sql diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index bf5cbd528..3180222bb 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1,4 +1,4 @@ - + - - - - - - - - - + <% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %> ProManager From 1bb2212e4aa1f9efdb7308c491fba77219360691 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 17 Sep 2024 12:55:08 -0700 Subject: [PATCH 09/13] IO-2921 CARSTAR Canada Chatter Datapump Signed-off-by: Allan Carr --- server/data/chatter.js | 171 +++++++++++++++++++++++++++++++ server/data/data.js | 1 + server/graphql-client/queries.js | 29 ++++++ server/routes/dataRoutes.js | 3 +- 4 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 server/data/chatter.js diff --git a/server/data/chatter.js b/server/data/chatter.js new file mode 100644 index 000000000..69c7707a8 --- /dev/null +++ b/server/data/chatter.js @@ -0,0 +1,171 @@ +const path = require("path"); +const queries = require("../graphql-client/queries"); +const moment = require("moment-timezone"); +const converter = require("json-2-csv"); +const _ = require("lodash"); +const logger = require("../utils/logger"); +const fs = require("fs"); +const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager"); +require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); +let Client = require("ssh2-sftp-client"); + +const client = require("../graphql-client/graphql-client").client; +const { sendServerEmail } = require("../email/sendemail"); + +const ftpSetup = { + host: process.env.CHATTER_HOST, + port: process.env.CHATTER_PORT, + username: process.env.CHATTER_USER, + privateKey: null, + debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), + algorithms: { + serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"] + } +}; + +exports.default = async (req, res) => { + // Only process if in production environment. + if (process.env.NODE_ENV !== "production") { + res.sendStatus(403); + return; + } + + //Query for the List of Bodyshop Clients. + logger.log("chatter-start", "DEBUG", "api", null, null); + const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); + + const specificShopIds = req.body.bodyshopIds; // ['uuid] + const { start, end, skipUpload } = req.body; //YYYY-MM-DD + if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { + res.sendStatus(401); + return; + } + const allcsvsToUpload = []; + const allErrors = []; + try { + for (const bodyshop of specificShopIds ? bodyshops.filter((b) => specificShopIds.includes(b.id)) : bodyshops) { + logger.log("chatter-start-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); + try { + const { jobs, bodyshops_by_pk } = await client.request(queries.CHATTER_QUERY, { + bodyshopid: bodyshop.id, + start: start ? moment(start).startOf("day") : moment().subtract(1, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }) + }); + + const chatterObject = jobs.map((j) => { + return { + poc_trigger_code: bodyshops_by_pk.chatterid, + firstname: j.ownr_co_nm ? null : j.ownr_fn, + lastname: j.ownr_co_nm ? j.ownr_co_nm : j.ownr_ln, + transaction_id: j.ro_number, + email: j.ownr_ea, + phone_number: j.ownr_ph1 + }; + }); + + var ret = converter.json2csv(chatterObject, { emptyFieldValue: "" }); + + allcsvsToUpload.push({ + count: chatterObject.length, + csv: ret, + filename: `${bodyshop.shopname}_solicitation_${moment().format("YYYYMMDD")}.csv` + }); + + logger.log("chatter-end-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname + }); + } catch (error) { + //Error at the shop level. + logger.log("chatter-error-shop", "ERROR", "api", bodyshop.id, { + ...error + }); + + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + fatal: true, + errors: [error.toString()] + }); + } finally { + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname + }); + } + } + + if (skipUpload) { + for (const csvObj of allcsvsToUpload) { + fs.writeFileSync(`./logs/${csvObj.filename}`, csvObj.csv); + } + + res.json(allcsvsToUpload); + sendServerEmail({ + subject: `Chatter Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )} + ` + }); + return; + } + + let sftp = new Client(); + sftp.on("error", (errors) => logger.log("chatter-sftp-error", "ERROR", "api", null, { ...errors })); + try { + //Get the private key from AWS Secrets Manager. + ftpSetup.privateKey = await getPrivateKey(); + + //Connect to the FTP and upload all. + await sftp.connect(ftpSetup); + + for (const csvObj of allcsvsToUpload) { + logger.log("chatter-sftp-upload", "DEBUG", "api", null, { filename: csvObj.filename }); + + const uploadResult = await sftp.put(Buffer.from(csvObj.xml), `/${csvObj.filename}`); + logger.log("chatter-sftp-upload-result", "DEBUG", "api", null, { uploadResult }); + } + + //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml + } catch (error) { + logger.log("chatter-sftp-error", "ERROR", "api", null, { ...error }); + } finally { + sftp.end(); + } + sendServerEmail({ + subject: `Chatter Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )}` + }); + res.sendStatus(200); + } catch (error) { + res.status(200).json(error); + } +}; + +async function getPrivateKey() { + // Connect to AWS Secrets Manager + const client = new SecretsManagerClient({ region: "ca-central-1" }); + const command = new GetSecretValueCommand({ SecretId: CHATTER_PRIVATE_KEY }); + + logger.log("chatter-get-private-key", "DEBUG", "api", null, null); + try { + const { SecretString, SecretBinary } = await client.send(command); + if (SecretString || SecretBinary) logger.log("chatter-retrieved-private-key", "DEBUG", "api", null, null); + return SecretString || Buffer.from(SecretBinary, "base64").toString("ascii"); + } catch (error) { + logger.log("chatter-get-private-key", "ERROR", "api", null, error); + throw err; + } +} diff --git a/server/data/data.js b/server/data/data.js index 1656c1878..0f6fcd30c 100644 --- a/server/data/data.js +++ b/server/data/data.js @@ -1,4 +1,5 @@ exports.arms = require("./arms").default; exports.autohouse = require("./autohouse").default; +exports.chatter = require("./chatter").default; exports.claimscorp = require("./claimscorp").default; exports.kaizen = require("./kaizen").default; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 65cbef473..423ca673b 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -832,6 +832,25 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop } }`; +exports.CHATTER_QUERY = `query CHATTER_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) { + bodyshops_by_pk(id: $bodyshopid){ + id + shopname + chatterid + timezone + } + jobs(where: {_and: [{converted: {_eq: true}}, {actual_delivery: {_gt: $start}}, {actual_delivery: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}, {_or: [{ownr_ph1: {_is_null: false}}, {ownr_ea: {_is_null: false}}]}]}) { + id + created_at + ro_number + ownr_fn + ownr_ln + ownr_co_nm + ownr_ph1 + ownr_ea + } +}`; + exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) { bodyshops_by_pk(id: $bodyshopid){ id @@ -1732,6 +1751,16 @@ exports.GET_AUTOHOUSE_SHOPS = `query GET_AUTOHOUSE_SHOPS { } }`; +exports.GET_CHATTER_SHOPS = `query GET_CHATTER_SHOPS { + bodyshops(where: {chatterid: {_is_null: false}, _or: {chatterid: {_neq: ""}}}){ + id + shopname + chatterid + imexshopid + timezone + } +}`; + exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS { bodyshops(where: {claimscorpid: {_is_null: false}, _or: {claimscorpid: {_neq: ""}}}){ id diff --git a/server/routes/dataRoutes.js b/server/routes/dataRoutes.js index d7fe6e640..a12563282 100644 --- a/server/routes/dataRoutes.js +++ b/server/routes/dataRoutes.js @@ -1,9 +1,10 @@ const express = require("express"); const router = express.Router(); -const { autohouse, claimscorp, kaizen } = require("../data/data"); +const { autohouse, claimscorp, chatter, kaizen } = require("../data/data"); router.post("/ah", autohouse); router.post("/cc", claimscorp); +router.post("/chatter", chatter); router.post("/kaizen", kaizen); module.exports = router; From 3cd3d7414d4832f82a6c472c68278ad62c3cc8a9 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 17 Sep 2024 14:08:35 -0700 Subject: [PATCH 10/13] IO-2939 CDK Local Tax for ImEX Instance Local Tax doesn't exist for ImEX Instance Signed-off-by: Allan Carr --- server/cdk/cdk-calculate-allocations.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/server/cdk/cdk-calculate-allocations.js b/server/cdk/cdk-calculate-allocations.js index a1dd8cb7f..ddac4ee4c 100644 --- a/server/cdk/cdk-calculate-allocations.js +++ b/server/cdk/cdk-calculate-allocations.js @@ -54,13 +54,6 @@ function calculateAllocations(connectionData, job) { deubg: true, args: [], imex: () => ({ - local: { - center: bodyshop.md_responsibility_centers.taxes.local.name, - sale: Dinero(job.job_totals.totals.local_tax), - cost: Dinero(), - profitCenter: bodyshop.md_responsibility_centers.taxes.local, - costCenter: bodyshop.md_responsibility_centers.taxes.local - }, state: { center: bodyshop.md_responsibility_centers.taxes.state.name, sale: Dinero(job.job_totals.totals.state_tax), From 1a5c71048cee78d60c19b0e9b435019426f346d1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 18 Sep 2024 10:04:45 -0700 Subject: [PATCH 11/13] IO-2921 CARSTAR Canada Requested Adjustments Signed-off-by: Allan Carr --- server/data/chatter.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/server/data/chatter.js b/server/data/chatter.js index 69c7707a8..85dbe4a9b 100644 --- a/server/data/chatter.js +++ b/server/data/chatter.js @@ -22,7 +22,6 @@ const ftpSetup = { serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"] } }; - exports.default = async (req, res) => { // Only process if in production environment. if (process.env.NODE_ENV !== "production") { @@ -30,16 +29,16 @@ exports.default = async (req, res) => { return; } - //Query for the List of Bodyshop Clients. - logger.log("chatter-start", "DEBUG", "api", null, null); - const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); - - const specificShopIds = req.body.bodyshopIds; // ['uuid] - const { start, end, skipUpload } = req.body; //YYYY-MM-DD if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { res.sendStatus(401); return; } + //Query for the List of Bodyshop Clients. + logger.log("chatter-start", "DEBUG", "api", null, null); + const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); + const specificShopIds = req.body.bodyshopIds; // ['uuid] + const { start, end, skipUpload } = req.body; //YYYY-MM-DD + const allcsvsToUpload = []; const allErrors = []; try { @@ -65,7 +64,7 @@ exports.default = async (req, res) => { }; }); - var ret = converter.json2csv(chatterObject, { emptyFieldValue: "" }); + const ret = converter.json2csv(chatterObject, { emptyFieldValue: "" }); allcsvsToUpload.push({ count: chatterObject.length, @@ -100,10 +99,9 @@ exports.default = async (req, res) => { if (skipUpload) { for (const csvObj of allcsvsToUpload) { - fs.writeFileSync(`./logs/${csvObj.filename}`, csvObj.csv); + fs.writeFile(`./logs/${csvObj.filename}`, csvObj.csv); } - res.json(allcsvsToUpload); sendServerEmail({ subject: `Chatter Report ${moment().format("MM-DD-YY")}`, text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} @@ -114,10 +112,11 @@ exports.default = async (req, res) => { )} ` }); + res.json(allcsvsToUpload); return; } - let sftp = new Client(); + const sftp = new Client(); sftp.on("error", (errors) => logger.log("chatter-sftp-error", "ERROR", "api", null, { ...errors })); try { //Get the private key from AWS Secrets Manager. @@ -132,8 +131,6 @@ exports.default = async (req, res) => { const uploadResult = await sftp.put(Buffer.from(csvObj.xml), `/${csvObj.filename}`); logger.log("chatter-sftp-upload-result", "DEBUG", "api", null, { uploadResult }); } - - //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml } catch (error) { logger.log("chatter-sftp-error", "ERROR", "api", null, { ...error }); } finally { From e3059b41ae782908d1aeffd227b232fa41496dd5 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 18 Sep 2024 11:19:43 -0700 Subject: [PATCH 12/13] IO-2933 Resolve PR comments. --- .../card-payment-modal.component..jsx | 8 +------- .../payments-generate-link.component.jsx | 2 +- server/email/tasksEmails.js | 7 +++---- server/intellipay/intellipay.js | 12 +++++------- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/client/src/components/card-payment-modal/card-payment-modal.component..jsx b/client/src/components/card-payment-modal/card-payment-modal.component..jsx index e5059d14a..29a95b87b 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.component..jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.component..jsx @@ -337,13 +337,7 @@ const CardPaymentModalComponent = ({ message.success(t("general.actions.copied")); }} > -
{ - //Copy the link. - }} - > - {paymentLink} -
+
{paymentLink}
)} diff --git a/client/src/components/payments-generate-link/payments-generate-link.component.jsx b/client/src/components/payments-generate-link/payments-generate-link.component.jsx index 5d19556c8..b0f4b26b9 100644 --- a/client/src/components/payments-generate-link/payments-generate-link.component.jsx +++ b/client/src/components/payments-generate-link/payments-generate-link.component.jsx @@ -35,7 +35,7 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope try { p = parsePhoneNumber(job.ownr_ph1 || "", "CA"); } catch (error) { - console.log("Unable to part phone number"); + console.log("Unable to parse phone number"); } setLoading(true); const response = await axios.post("/intellipay/generate_payment_url", { diff --git a/server/email/tasksEmails.js b/server/email/tasksEmails.js index 292d7c92e..a106085f7 100644 --- a/server/email/tasksEmails.js +++ b/server/email/tasksEmails.js @@ -95,8 +95,8 @@ const formatPriority = (priority) => { * @returns {{header, body: string, subHeader: string}} */ -function getEndpoints() { - const endPoints = InstanceManager({ +const getEndpoints = () => + InstanceManager({ imex: process.env?.NODE_ENV === "test" ? "https://test.imex.online" : "https://imex.online", rome: bodyshop.convenient_company === "promanager" @@ -107,8 +107,7 @@ function getEndpoints() { ? "https//test.romeonline.io" : "https://romeonline.io" }); - return endPoints; -} + const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, job, taskId) => { const endPoints = getEndpoints(); return { diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index 831ade65c..f798bc0bd 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -172,12 +172,9 @@ exports.postback = async (req, res) => { //Adding in the user email to the short pay email. //Need to check this to ensure backwards compatibility for clients that don't update. - let partialPayments; - if (!Array.isArray(parsedComment)) { - partialPayments = parsedComment.payments; - } else { - partialPayments = parsedComment; - } + + const partialPayments = Array.isArray(parsedComment) ? parsedComment : parsedComment.payments; + const jobs = await gqlClient.request(queries.GET_JOBS_BY_PKS, { ids: partialPayments.map((p) => p.jobid) }); @@ -207,7 +204,7 @@ exports.postback = async (req, res) => { iprequest: values, paymentResult }); - res.sendStatus(200); + if (values.origin === "OneLink" && parsedComment.userEmail) { //Send an email, it was a text to pay link. const endPoints = getEndpoints(); @@ -226,6 +223,7 @@ exports.postback = async (req, res) => { .join("
") }) }); + res.sendStatus(200); } } else if (values.invoice) { //This is a link email that's been sent out. From eeed004fe246dd7b543b8c48f26070a43dc2d842 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 18 Sep 2024 15:00:32 -0400 Subject: [PATCH 13/13] 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,