From 30cb4ef5620b43a389ac8aa90cafb545062121bf Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 16 Sep 2024 11:06:56 -0700 Subject: [PATCH 01/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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, From 29f0031c1e092ced78a9044871e499840e119142 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 18 Sep 2024 18:30:02 -0400 Subject: [PATCH 14/26] IO-2782-Send-Promanager-Welcome-Email - Send ProManager welcome email Signed-off-by: Dave Richer --- server/email/sendemail.js | 15 ++++ server/firebase/firebase-handler.js | 122 +++++++++++++++++++++++++++- server/routes/adminRoutes.js | 1 + 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/server/email/sendemail.js b/server/email/sendemail.js index 85534b815..878918f48 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -96,6 +96,20 @@ const sendServerEmail = async ({ subject, text }) => { } }; +const sendProManagerWelcomeEmail = async (to, subject, html) => { + try { + await transporter.sendMail({ + from: `ProManager `, + to, + subject, + html + }); + } catch (error) { + console.log(error); + logger.log("server-email-failure", "error", null, null, error); + } +}; + const sendTaskEmail = async ({ to, subject, text, attachments }) => { try { transporter.sendMail( @@ -309,5 +323,6 @@ module.exports = { sendEmail, sendServerEmail, sendTaskEmail, + sendProManagerWelcomeEmail, emailBounce }; diff --git a/server/firebase/firebase-handler.js b/server/firebase/firebase-handler.js index 509bedce9..2cee624dd 100644 --- a/server/firebase/firebase-handler.js +++ b/server/firebase/firebase-handler.js @@ -1,7 +1,7 @@ const admin = require("firebase-admin"); const logger = require("../utils/logger"); const path = require("path"); -const { auth } = require("firebase-admin"); +const { sendProManagerWelcomeEmail } = require("../email/sendemail"); require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) @@ -10,6 +10,7 @@ const client = require("../graphql-client/graphql-client").client; const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON); const adminEmail = require("../utils/adminEmail"); +const generateEmailTemplate = require("../email/generateTemplate"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), @@ -24,7 +25,8 @@ exports.createUser = async (req, res) => { ioadmin: true }); - const { email, displayName, password, shopid, authlevel } = req.body; + const { email, displayName, password, shopid, authlevel, validemail } = req.body; + try { const userRecord = await admin.auth().createUser({ email, displayName, password }); @@ -42,6 +44,7 @@ exports.createUser = async (req, res) => { user: { email: email.toLowerCase(), authid: userRecord.uid, + validemail, associations: { data: [{ shopid, authlevel, active: true }] } @@ -58,6 +61,121 @@ exports.createUser = async (req, res) => { } }; +exports.promanagerWelcomeEmail = (req, res) => { + const { authid, email } = req.body; + + // Gate the operation to only admin users + if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { + logger.log("admin-update-user-unauthorized", "ERROR", req.user.email, null, { + request: req.body, + user: req.user + }); + res.sendStatus(404); + return; + } + + admin + .auth() + .getUser(authid) + .then((userRecord) => { + if (!userRecord) { + res.status(404).json({ message: "User not found in Firebase." }); + return Promise.reject("User not found in Firebase."); + } + + // Fetch user data from the database using GraphQL + return client.request( + ` + query GET_USER_BY_EMAIL($email: String!) { + users(where: { email: { _eq: $email } }) { + email + validemail + associations { + id + shopid + bodyshop { + id + convenient_company + } + } + } + } + `, + { email: email.toLowerCase() } + ); + }) + .then((dbUserResult) => { + const dbUser = dbUserResult?.users?.[0]; + if (!dbUser) { + res.status(404).json({ message: "User not found in database." }); + return Promise.reject("User not found in database."); + } + + // Check if the email is valid before proceeding + if (!dbUser.validemail) { + logger.log("admin-send-welcome-email-skip", "ADMIN", req.user.email, null, { + message: "User email is not valid, skipping email.", + email + }); + return res.status(200).json({ message: "User email is not valid, email not sent." }); + } + + // Check if convenient_company is equal to "promanager" + const convenientCompany = dbUser.associations?.[0]?.bodyshop?.convenient_company; + if (convenientCompany !== "promanager") { + logger.log("admin-send-welcome-email-skip", "ADMIN", req.user.email, null, { + message: `convenient_company is not "promanager", skipping email.`, + convenientCompany + }); + return res.status(200).json({ message: `convenient_company is not "promanager", email not sent.` }); + } + + // Generate password reset link + return admin + .auth() + .generatePasswordResetLink(dbUser.email) + .then((resetLink) => ({ + dbUser, + resetLink + })); + }) + .then(({ dbUser, resetLink }) => { + // Send email logic here (replace this with your email-sending service) + return sendProManagerWelcomeEmail({ + to: dbUser.email, + subject: "Welcome to the ProManager platform.", + html: generateEmailTemplate({ + header: "", + subHeader: "", + body: ` +

Welcome to the ProManager platform. Please click the link below to reset your password:

+

Reset your password

+

User Details:

+
    +
  • Email: ${dbUser.email}
  • +
+ ` + }) + }); + }) + .then(() => { + logger.log("admin-send-welcome-email", "ADMIN", req.user.email, null, { + request: req.body, + ioadmin: true, + emailSentTo: email + }); + res.status(200).json({ message: "Welcome email sent successfully." }); + }) + .catch((error) => { + logger.log("admin-send-welcome-email-error", "ERROR", req.user.email, null, { + error + }); + if (!res.headersSent) { + res.status(500).json({ message: "Error sending welcome email.", error }); + } + }); +}; + exports.updateUser = (req, res) => { logger.log("admin-update-user", "ADMIN", req.user.email, null, { request: req.body, diff --git a/server/routes/adminRoutes.js b/server/routes/adminRoutes.js index c1d3fe85a..74775724d 100644 --- a/server/routes/adminRoutes.js +++ b/server/routes/adminRoutes.js @@ -14,5 +14,6 @@ router.post("/updatecounter", validateAdminMiddleware, updateCounter); router.post("/updateuser", fb.updateUser); router.post("/getuser", fb.getUser); router.post("/createuser", fb.createUser); +router.post("/promanagerwelcome", fb.promanagerWelcomeEmail); module.exports = router; From cdb2d4d2d6fd3da3fe807431598e8ea55a929064 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 19 Sep 2024 11:29:13 -0400 Subject: [PATCH 15/26] IO-2782-Send-Promanager-Welcome-Email - Cleanup of adminRoutes / firebase-handler.js Signed-off-by: Dave Richer --- server/firebase/firebase-handler.js | 129 ++++++++++++---------------- server/routes/adminRoutes.js | 19 ++-- 2 files changed, 64 insertions(+), 84 deletions(-) diff --git a/server/firebase/firebase-handler.js b/server/firebase/firebase-handler.js index 2cee624dd..7223d89a8 100644 --- a/server/firebase/firebase-handler.js +++ b/server/firebase/firebase-handler.js @@ -1,11 +1,11 @@ -const admin = require("firebase-admin"); -const logger = require("../utils/logger"); const path = require("path"); -const { sendProManagerWelcomeEmail } = require("../email/sendemail"); - require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); + +const admin = require("firebase-admin"); +const logger = require("../utils/logger"); +const { sendProManagerWelcomeEmail } = require("../email/sendemail"); const client = require("../graphql-client/graphql-client").client; const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON); @@ -17,9 +17,7 @@ admin.initializeApp({ databaseURL: process.env.FIREBASE_DATABASE_URL }); -exports.admin = admin; - -exports.createUser = async (req, res) => { +const createUser = async (req, res) => { logger.log("admin-create-user", "ADMIN", req.user.email, null, { request: req.body, ioadmin: true @@ -61,57 +59,45 @@ exports.createUser = async (req, res) => { } }; -exports.promanagerWelcomeEmail = (req, res) => { +const sendPromanagerWelcomeEmail = (req, res) => { const { authid, email } = req.body; - // Gate the operation to only admin users - if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { - logger.log("admin-update-user-unauthorized", "ERROR", req.user.email, null, { - request: req.body, - user: req.user - }); - res.sendStatus(404); - return; - } - + // Fetch user from Firebase admin .auth() .getUser(authid) .then((userRecord) => { if (!userRecord) { - res.status(404).json({ message: "User not found in Firebase." }); - return Promise.reject("User not found in Firebase."); + return Promise.reject({ status: 404, message: "User not found in Firebase." }); } // Fetch user data from the database using GraphQL return client.request( ` - query GET_USER_BY_EMAIL($email: String!) { - users(where: { email: { _eq: $email } }) { - email - validemail - associations { + query GET_USER_BY_EMAIL($email: String!) { + users(where: { email: { _eq: $email } }) { + email + validemail + associations { + id + shopid + bodyshop { id - shopid - bodyshop { - id - convenient_company - } + convenient_company } } } - `, + }`, { email: email.toLowerCase() } ); }) .then((dbUserResult) => { const dbUser = dbUserResult?.users?.[0]; if (!dbUser) { - res.status(404).json({ message: "User not found in database." }); - return Promise.reject("User not found in database."); + return Promise.reject({ status: 404, message: "User not found in database." }); } - // Check if the email is valid before proceeding + // Validate email before proceeding if (!dbUser.validemail) { logger.log("admin-send-welcome-email-skip", "ADMIN", req.user.email, null, { message: "User email is not valid, skipping email.", @@ -120,11 +106,11 @@ exports.promanagerWelcomeEmail = (req, res) => { return res.status(200).json({ message: "User email is not valid, email not sent." }); } - // Check if convenient_company is equal to "promanager" + // Check if the user's company is ProManager const convenientCompany = dbUser.associations?.[0]?.bodyshop?.convenient_company; if (convenientCompany !== "promanager") { logger.log("admin-send-welcome-email-skip", "ADMIN", req.user.email, null, { - message: `convenient_company is not "promanager", skipping email.`, + message: 'convenient_company is not "promanager", skipping email.', convenientCompany }); return res.status(200).json({ message: `convenient_company is not "promanager", email not sent.` }); @@ -134,13 +120,10 @@ exports.promanagerWelcomeEmail = (req, res) => { return admin .auth() .generatePasswordResetLink(dbUser.email) - .then((resetLink) => ({ - dbUser, - resetLink - })); + .then((resetLink) => ({ dbUser, resetLink })); }) .then(({ dbUser, resetLink }) => { - // Send email logic here (replace this with your email-sending service) + // Send welcome email (replace with your actual email-sending service) return sendProManagerWelcomeEmail({ to: dbUser.email, subject: "Welcome to the ProManager platform.", @@ -148,17 +131,18 @@ exports.promanagerWelcomeEmail = (req, res) => { header: "", subHeader: "", body: ` -

Welcome to the ProManager platform. Please click the link below to reset your password:

-

Reset your password

-

User Details:

-
    -
  • Email: ${dbUser.email}
  • -
- ` +

Welcome to the ProManager platform. Please click the link below to reset your password:

+

Reset your password

+

User Details:

+
    +
  • Email: ${dbUser.email}
  • +
+ ` }) }); }) .then(() => { + // Log success and return response logger.log("admin-send-welcome-email", "ADMIN", req.user.email, null, { request: req.body, ioadmin: true, @@ -167,30 +151,23 @@ exports.promanagerWelcomeEmail = (req, res) => { res.status(200).json({ message: "Welcome email sent successfully." }); }) .catch((error) => { - logger.log("admin-send-welcome-email-error", "ERROR", req.user.email, null, { - error - }); + logger.log("admin-send-welcome-email-error", "ERROR", req.user.email, null, { error }); + if (!res.headersSent) { - res.status(500).json({ message: "Error sending welcome email.", error }); + res.status(error.status || 500).json({ + message: error.message || "Error sending welcome email.", + error + }); } }); }; -exports.updateUser = (req, res) => { +const updateUser = (req, res) => { logger.log("admin-update-user", "ADMIN", req.user.email, null, { request: req.body, ioadmin: true }); - if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { - logger.log("admin-update-user-unauthorized", "ERROR", req.user.email, null, { - request: req.body, - user: req.user - }); - res.sendStatus(404); - return; - } - admin .auth() .updateUser( @@ -223,21 +200,12 @@ exports.updateUser = (req, res) => { }); }; -exports.getUser = (req, res) => { +const getUser = (req, res) => { logger.log("admin-get-user", "ADMIN", req.user.email, null, { request: req.body, ioadmin: true }); - if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { - logger.log("admin-update-user-unauthorized", "ERROR", req.user.email, null, { - request: req.body, - user: req.user - }); - res.sendStatus(404); - return; - } - admin .auth() .getUser(req.body.uid) @@ -252,7 +220,7 @@ exports.getUser = (req, res) => { }); }; -exports.sendNotification = async (req, res) => { +const sendNotification = async (req, res) => { setTimeout(() => { // Send a message to the device corresponding to the provided // registration token. @@ -285,7 +253,7 @@ exports.sendNotification = async (req, res) => { }, 500); }; -exports.subscribe = async (req, res) => { +const subscribe = async (req, res) => { const result = await admin .messaging() .subscribeToTopic(req.body.fcm_tokens, `${req.body.imexshopid}-${req.body.type}`); @@ -293,7 +261,7 @@ exports.subscribe = async (req, res) => { res.json(result); }; -exports.unsubscribe = async (req, res) => { +const unsubscribe = async (req, res) => { try { const result = await admin .messaging() @@ -305,6 +273,17 @@ exports.unsubscribe = async (req, res) => { } }; +module.exports = { + admin, + createUser, + updateUser, + getUser, + sendPromanagerWelcomeEmail, + sendNotification, + subscribe, + unsubscribe +}; + //Admin claims code. // const uid = "JEqqYlsadwPEXIiyRBR55fflfko1"; diff --git a/server/routes/adminRoutes.js b/server/routes/adminRoutes.js index 74775724d..ac0ebb6fb 100644 --- a/server/routes/adminRoutes.js +++ b/server/routes/adminRoutes.js @@ -1,19 +1,20 @@ const express = require("express"); const router = express.Router(); -const fb = require("../firebase/firebase-handler"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const { createAssociation, createShop, updateShop, updateCounter } = require("../admin/adminops"); +const { updateUser, getUser, createUser, sendPromanagerWelcomeEmail } = require("../firebase/firebase-handler"); const validateAdminMiddleware = require("../middleware/validateAdminMiddleware"); router.use(validateFirebaseIdTokenMiddleware); +router.use(validateAdminMiddleware); -router.post("/createassociation", validateAdminMiddleware, createAssociation); -router.post("/createshop", validateAdminMiddleware, createShop); -router.post("/updateshop", validateAdminMiddleware, updateShop); -router.post("/updatecounter", validateAdminMiddleware, updateCounter); -router.post("/updateuser", fb.updateUser); -router.post("/getuser", fb.getUser); -router.post("/createuser", fb.createUser); -router.post("/promanagerwelcome", fb.promanagerWelcomeEmail); +router.post("/createassociation", createAssociation); +router.post("/createshop", createShop); +router.post("/updateshop", updateShop); +router.post("/updatecounter", updateCounter); +router.post("/updateuser", updateUser); +router.post("/getuser", getUser); +router.post("/createuser", createUser); +router.post("/promanagerwelcome", sendPromanagerWelcomeEmail); module.exports = router; From c09e22ed967a77d27b202cd5a3303de468fc5f20 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 19 Sep 2024 09:45:32 -0700 Subject: [PATCH 16/26] IO-2948 Production Job Status Filter Signed-off-by: Allan Carr --- .../production-list-columns.data.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/components/production-list-columns/production-list-columns.data.jsx b/client/src/components/production-list-columns/production-list-columns.data.jsx index 16a450dab..950d956af 100644 --- a/client/src/components/production-list-columns/production-list-columns.data.jsx +++ b/client/src/components/production-list-columns/production-list-columns.data.jsx @@ -298,6 +298,16 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme ellipsis: true, sorter: (a, b) => statusSort(a.status, b.status, activeStatuses), sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filters: + activeStatuses + ?.map((s) => { + return { + text: s || "No Status*", + value: [s] + }; + }) + .sort((a, b) => statusSort(a.text, b.text, activeStatuses)) || [], + onFilter: (value, record) => value.includes(record.status), render: (text, record) => }, { From 145cf7cc9367c9322c333b0dd897525579b6762f Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 19 Sep 2024 12:54:26 -0400 Subject: [PATCH 17/26] IO-2782-Send-Promanager-Welcome-Email - Finalize cleanup Signed-off-by: Dave Richer --- server/firebase/firebase-handler.js | 33 ++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/server/firebase/firebase-handler.js b/server/firebase/firebase-handler.js index 7223d89a8..e47eeffc1 100644 --- a/server/firebase/firebase-handler.js +++ b/server/firebase/firebase-handler.js @@ -7,9 +7,7 @@ const admin = require("firebase-admin"); const logger = require("../utils/logger"); const { sendProManagerWelcomeEmail } = require("../email/sendemail"); const client = require("../graphql-client/graphql-client").client; - const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON); -const adminEmail = require("../utils/adminEmail"); const generateEmailTemplate = require("../email/generateTemplate"); admin.initializeApp({ @@ -210,7 +208,36 @@ const getUser = (req, res) => { .auth() .getUser(req.body.uid) .then((userRecord) => { - res.json(userRecord); + return client + .request( + ` + query GET_USER_BY_AUTHID($authid: String!) { + users(where: { authid: { _eq: $authid } }) { + email + displayName + validemail + associations { + id + shopid + bodyshop { + id + convenient_company + } + } + } + } + `, + { authid: req.body.uid } + ) + .then((dbUserResult) => { + res.json({ + ...userRecord, + db: { + validemail: dbUserResult?.users?.[0]?.validemail, + company: dbUserResult?.users?.[0]?.associations?.[0]?.bodyshop?.convenient_company + } + }); + }); }) .catch((error) => { logger.log("admin-get-user-error", "ERROR", req.user.email, null, { From 4ad87a522c0ab2b5e4ae78c0927b4d393ad1dfff Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 19 Sep 2024 13:08:23 -0400 Subject: [PATCH 18/26] IO-2782-Send-Promanager-Welcome-Email - Update for merge conflict Signed-off-by: Dave Richer --- server/email/sendemail.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/email/sendemail.js b/server/email/sendemail.js index 878918f48..65b5a60ce 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -110,7 +110,7 @@ const sendProManagerWelcomeEmail = async (to, subject, html) => { } }; -const sendTaskEmail = async ({ to, subject, text, attachments }) => { +const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachments }) => { try { transporter.sendMail( { @@ -121,7 +121,7 @@ const sendTaskEmail = async ({ to, subject, text, attachments }) => { }), to: to, subject: subject, - text: text, + ...(type === "text" ? { text } : { html }), attachments: attachments || null }, (err, info) => { From cc30ea658ef2ccc06f57de3e720357a2bfa4516a Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 19 Sep 2024 12:07:24 -0700 Subject: [PATCH 19/26] Hasura index removal. --- .../1726768560738_drop_index_courtesycars_idx_fleet/down.sql | 2 ++ .../1726768560738_drop_index_courtesycars_idx_fleet/up.sql | 1 + .../1726768733548_drop_index_idx_jobs_ownrfn/down.sql | 2 ++ .../migrations/1726768733548_drop_index_idx_jobs_ownrfn/up.sql | 1 + .../1726768747444_drop_index_idx_jobs_ownrln/down.sql | 2 ++ .../migrations/1726768747444_drop_index_idx_jobs_ownrln/up.sql | 1 + .../1726768755516_drop_index_jobs_idx_iouparent/down.sql | 2 ++ .../1726768755516_drop_index_jobs_idx_iouparent/up.sql | 1 + .../1726768776395_drop_index_idx_jobs_ronumber/down.sql | 2 ++ .../1726768776395_drop_index_idx_jobs_ronumber/up.sql | 1 + .../migrations/1726768781889_drop_index_idx_jobs_clmno/down.sql | 2 ++ .../migrations/1726768781889_drop_index_idx_jobs_clmno/up.sql | 1 + .../1726768808135_drop_index_idx_jobs_vmodeldesc/down.sql | 2 ++ .../1726768808135_drop_index_idx_jobs_vmodeldesc/up.sql | 1 + .../1726768818475_drop_index_idx_jobs_vmakedesc/down.sql | 2 ++ .../1726768818475_drop_index_idx_jobs_vmakedesc/up.sql | 1 + .../1726768826203_drop_index_idx_jobs_plateno/down.sql | 2 ++ .../migrations/1726768826203_drop_index_idx_jobs_plateno/up.sql | 1 + 18 files changed, 27 insertions(+) create mode 100644 hasura/migrations/1726768560738_drop_index_courtesycars_idx_fleet/down.sql create mode 100644 hasura/migrations/1726768560738_drop_index_courtesycars_idx_fleet/up.sql create mode 100644 hasura/migrations/1726768733548_drop_index_idx_jobs_ownrfn/down.sql create mode 100644 hasura/migrations/1726768733548_drop_index_idx_jobs_ownrfn/up.sql create mode 100644 hasura/migrations/1726768747444_drop_index_idx_jobs_ownrln/down.sql create mode 100644 hasura/migrations/1726768747444_drop_index_idx_jobs_ownrln/up.sql create mode 100644 hasura/migrations/1726768755516_drop_index_jobs_idx_iouparent/down.sql create mode 100644 hasura/migrations/1726768755516_drop_index_jobs_idx_iouparent/up.sql create mode 100644 hasura/migrations/1726768776395_drop_index_idx_jobs_ronumber/down.sql create mode 100644 hasura/migrations/1726768776395_drop_index_idx_jobs_ronumber/up.sql create mode 100644 hasura/migrations/1726768781889_drop_index_idx_jobs_clmno/down.sql create mode 100644 hasura/migrations/1726768781889_drop_index_idx_jobs_clmno/up.sql create mode 100644 hasura/migrations/1726768808135_drop_index_idx_jobs_vmodeldesc/down.sql create mode 100644 hasura/migrations/1726768808135_drop_index_idx_jobs_vmodeldesc/up.sql create mode 100644 hasura/migrations/1726768818475_drop_index_idx_jobs_vmakedesc/down.sql create mode 100644 hasura/migrations/1726768818475_drop_index_idx_jobs_vmakedesc/up.sql create mode 100644 hasura/migrations/1726768826203_drop_index_idx_jobs_plateno/down.sql create mode 100644 hasura/migrations/1726768826203_drop_index_idx_jobs_plateno/up.sql diff --git a/hasura/migrations/1726768560738_drop_index_courtesycars_idx_fleet/down.sql b/hasura/migrations/1726768560738_drop_index_courtesycars_idx_fleet/down.sql new file mode 100644 index 000000000..0716812f8 --- /dev/null +++ b/hasura/migrations/1726768560738_drop_index_courtesycars_idx_fleet/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "courtesycars_idx_fleet" on + "public"."courtesycars" using btree ("fleetnumber"); diff --git a/hasura/migrations/1726768560738_drop_index_courtesycars_idx_fleet/up.sql b/hasura/migrations/1726768560738_drop_index_courtesycars_idx_fleet/up.sql new file mode 100644 index 000000000..9a23b8690 --- /dev/null +++ b/hasura/migrations/1726768560738_drop_index_courtesycars_idx_fleet/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."courtesycars_idx_fleet"; diff --git a/hasura/migrations/1726768733548_drop_index_idx_jobs_ownrfn/down.sql b/hasura/migrations/1726768733548_drop_index_idx_jobs_ownrfn/down.sql new file mode 100644 index 000000000..81e52f24a --- /dev/null +++ b/hasura/migrations/1726768733548_drop_index_idx_jobs_ownrfn/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_ownrfn" on + "public"."jobs" using gin ("ownr_fn"); diff --git a/hasura/migrations/1726768733548_drop_index_idx_jobs_ownrfn/up.sql b/hasura/migrations/1726768733548_drop_index_idx_jobs_ownrfn/up.sql new file mode 100644 index 000000000..9d362a787 --- /dev/null +++ b/hasura/migrations/1726768733548_drop_index_idx_jobs_ownrfn/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_ownrfn"; diff --git a/hasura/migrations/1726768747444_drop_index_idx_jobs_ownrln/down.sql b/hasura/migrations/1726768747444_drop_index_idx_jobs_ownrln/down.sql new file mode 100644 index 000000000..a0bdd4596 --- /dev/null +++ b/hasura/migrations/1726768747444_drop_index_idx_jobs_ownrln/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_ownrln" on + "public"."jobs" using gin ("ownr_ln"); diff --git a/hasura/migrations/1726768747444_drop_index_idx_jobs_ownrln/up.sql b/hasura/migrations/1726768747444_drop_index_idx_jobs_ownrln/up.sql new file mode 100644 index 000000000..2a37d05c2 --- /dev/null +++ b/hasura/migrations/1726768747444_drop_index_idx_jobs_ownrln/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_ownrln"; diff --git a/hasura/migrations/1726768755516_drop_index_jobs_idx_iouparent/down.sql b/hasura/migrations/1726768755516_drop_index_jobs_idx_iouparent/down.sql new file mode 100644 index 000000000..8f0d5986f --- /dev/null +++ b/hasura/migrations/1726768755516_drop_index_jobs_idx_iouparent/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "jobs_idx_iouparent" on + "public"."jobs" using btree ("iouparent"); diff --git a/hasura/migrations/1726768755516_drop_index_jobs_idx_iouparent/up.sql b/hasura/migrations/1726768755516_drop_index_jobs_idx_iouparent/up.sql new file mode 100644 index 000000000..f0b2c7068 --- /dev/null +++ b/hasura/migrations/1726768755516_drop_index_jobs_idx_iouparent/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."jobs_idx_iouparent"; diff --git a/hasura/migrations/1726768776395_drop_index_idx_jobs_ronumber/down.sql b/hasura/migrations/1726768776395_drop_index_idx_jobs_ronumber/down.sql new file mode 100644 index 000000000..bbcb35060 --- /dev/null +++ b/hasura/migrations/1726768776395_drop_index_idx_jobs_ronumber/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_ronumber" on + "public"."jobs" using gin ("ro_number"); diff --git a/hasura/migrations/1726768776395_drop_index_idx_jobs_ronumber/up.sql b/hasura/migrations/1726768776395_drop_index_idx_jobs_ronumber/up.sql new file mode 100644 index 000000000..62b19c60b --- /dev/null +++ b/hasura/migrations/1726768776395_drop_index_idx_jobs_ronumber/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_ronumber"; diff --git a/hasura/migrations/1726768781889_drop_index_idx_jobs_clmno/down.sql b/hasura/migrations/1726768781889_drop_index_idx_jobs_clmno/down.sql new file mode 100644 index 000000000..043f2e639 --- /dev/null +++ b/hasura/migrations/1726768781889_drop_index_idx_jobs_clmno/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_clmno" on + "public"."jobs" using gin ("clm_no"); diff --git a/hasura/migrations/1726768781889_drop_index_idx_jobs_clmno/up.sql b/hasura/migrations/1726768781889_drop_index_idx_jobs_clmno/up.sql new file mode 100644 index 000000000..b2a68ac13 --- /dev/null +++ b/hasura/migrations/1726768781889_drop_index_idx_jobs_clmno/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_clmno"; diff --git a/hasura/migrations/1726768808135_drop_index_idx_jobs_vmodeldesc/down.sql b/hasura/migrations/1726768808135_drop_index_idx_jobs_vmodeldesc/down.sql new file mode 100644 index 000000000..4ced5c891 --- /dev/null +++ b/hasura/migrations/1726768808135_drop_index_idx_jobs_vmodeldesc/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_vmodeldesc" on + "public"."jobs" using gin ("v_model_desc"); diff --git a/hasura/migrations/1726768808135_drop_index_idx_jobs_vmodeldesc/up.sql b/hasura/migrations/1726768808135_drop_index_idx_jobs_vmodeldesc/up.sql new file mode 100644 index 000000000..fd2b28d83 --- /dev/null +++ b/hasura/migrations/1726768808135_drop_index_idx_jobs_vmodeldesc/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_vmodeldesc"; diff --git a/hasura/migrations/1726768818475_drop_index_idx_jobs_vmakedesc/down.sql b/hasura/migrations/1726768818475_drop_index_idx_jobs_vmakedesc/down.sql new file mode 100644 index 000000000..4f896b75e --- /dev/null +++ b/hasura/migrations/1726768818475_drop_index_idx_jobs_vmakedesc/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_vmakedesc" on + "public"."jobs" using gin ("v_make_desc"); diff --git a/hasura/migrations/1726768818475_drop_index_idx_jobs_vmakedesc/up.sql b/hasura/migrations/1726768818475_drop_index_idx_jobs_vmakedesc/up.sql new file mode 100644 index 000000000..cd34c044a --- /dev/null +++ b/hasura/migrations/1726768818475_drop_index_idx_jobs_vmakedesc/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_vmakedesc"; diff --git a/hasura/migrations/1726768826203_drop_index_idx_jobs_plateno/down.sql b/hasura/migrations/1726768826203_drop_index_idx_jobs_plateno/down.sql new file mode 100644 index 000000000..85248ec4f --- /dev/null +++ b/hasura/migrations/1726768826203_drop_index_idx_jobs_plateno/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_plateno" on + "public"."jobs" using gin ("plate_no"); diff --git a/hasura/migrations/1726768826203_drop_index_idx_jobs_plateno/up.sql b/hasura/migrations/1726768826203_drop_index_idx_jobs_plateno/up.sql new file mode 100644 index 000000000..a1a9b22f8 --- /dev/null +++ b/hasura/migrations/1726768826203_drop_index_idx_jobs_plateno/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_plateno"; From 4c0a1960ad10b3d050830798e20fb981a30910ee Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 20 Sep 2024 09:37:09 -0700 Subject: [PATCH 20/26] IO-2928 Remove Tax Code Ref if QBO US in Canada Signed-off-by: Allan Carr --- server/accounting/qbo/qbo-payables.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 1d16b0daf..30f5b1f62 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -194,7 +194,9 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) bodyshop.md_responsibility_centers.sales_tax_codes, classes, taxCodes, - bodyshop.md_responsibility_centers.costs + bodyshop.md_responsibility_centers.costs, + bodyshop.accountingconfig, + bodyshop.region_config ) ); @@ -298,17 +300,29 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) // }, // ], -const generateBillLine = (billLine, accounts, jobClass, ioSalesTaxCodes, classes, taxCodes, costCenters) => { +const generateBillLine = ( + billLine, + accounts, + jobClass, + ioSalesTaxCodes, + classes, + taxCodes, + costCenters, + accountingconfig, + region_config +) => { const account = costCenters.find((c) => c.name === billLine.cost_center); - return { DetailType: "AccountBasedExpenseLineDetail", AccountBasedExpenseLineDetail: { ...(jobClass ? { ClassRef: { value: classes[jobClass] } } : {}), - TaxCodeRef: { - value: taxCodes[findTaxCode(billLine.applicable_taxes, ioSalesTaxCodes)] - }, + TaxCodeRef: + accountingconfig.qbo && accountingconfig.qbo_usa && region_config.includes("CA_") + ? {} + : { + value: taxCodes[findTaxCode(billLine.applicable_taxes, ioSalesTaxCodes)] + }, AccountRef: { value: accounts[account.accountname] } From bece3278f450701dcb01fdd8e529bc2801ad620f Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 20 Sep 2024 09:50:08 -0700 Subject: [PATCH 21/26] IO-2949 change fetch policy on client to resolve issue. --- client/src/redux/messaging/messaging.sagas.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/redux/messaging/messaging.sagas.js b/client/src/redux/messaging/messaging.sagas.js index 16a1fd274..86757a427 100644 --- a/client/src/redux/messaging/messaging.sagas.js +++ b/client/src/redux/messaging/messaging.sagas.js @@ -36,7 +36,8 @@ export function* openChatByPhone({ payload }) { data: { conversations } } = yield client.query({ query: CONVERSATION_ID_BY_PHONE, - variables: { phone: p.number } + variables: { phone: p.number }, + fetchPolicy: 'no-cache' }); if (conversations.length === 0) { From 8ad39fe8557cc996a1fbb92a92bd9e97d2604c2a Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 20 Sep 2024 09:59:03 -0700 Subject: [PATCH 22/26] Add Git SHA date to ioevent. --- client/src/firebase/firebase.utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/firebase/firebase.utils.js b/client/src/firebase/firebase.utils.js index 4d3d65ca4..358182e6b 100644 --- a/client/src/firebase/firebase.utils.js +++ b/client/src/firebase/firebase.utils.js @@ -87,7 +87,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => { operationName: eventName, variables: additionalParams, dbevent: false, - env: "master" + env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}` }); // console.log( // "%c[Analytics]", From f3265901b6b39981b45cfdb436791739058ea075 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 20 Sep 2024 13:30:23 -0400 Subject: [PATCH 23/26] Adhoc - Add PR Helper Utility to Reference directory Signed-off-by: Dave Richer --- _reference/prHelper.html | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 _reference/prHelper.html diff --git a/_reference/prHelper.html b/_reference/prHelper.html new file mode 100644 index 000000000..fd5ad7c71 --- /dev/null +++ b/_reference/prHelper.html @@ -0,0 +1,59 @@ + + + + + + IMEX IO Extractor + + + +

IMEX IO Extractor

+ +
+ + +
+ + + + + From 70d857bfec308fbc64b385c2fa0e4d0ea0b5fe80 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 20 Sep 2024 13:14:15 -0700 Subject: [PATCH 24/26] IO-2933 resolve missing account details on --- .../card-payment-modal/card-payment-modal.component..jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 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 29a95b87b..fcdbd417c 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 @@ -45,7 +45,7 @@ const CardPaymentModalComponent = ({ const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, { variables: { jobids: [context.jobid] }, - skip: true + skip: !context?.jobid }); //Initialize the intellipay window. @@ -244,7 +244,8 @@ const CardPaymentModalComponent = ({ - prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join() + prevValues.payments?.map((p) => p?.jobid + p?.amount).join() !== + curValues.payments?.map((p) => p?.jobid + p?.amount).join() } > {() => { From f018a2b2a6e7168b755df8843159105a456dddf3 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 20 Sep 2024 14:57:31 -0700 Subject: [PATCH 25/26] Add additional Hasura Indexes --- hasura/migrations/1726868398933_run_sql_migration/down.sql | 3 +++ hasura/migrations/1726868398933_run_sql_migration/up.sql | 1 + .../1726868654375_create_index_idx_jobs_vehicleid/down.sql | 1 + .../1726868654375_create_index_idx_jobs_vehicleid/up.sql | 2 ++ 4 files changed, 7 insertions(+) create mode 100644 hasura/migrations/1726868398933_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726868398933_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726868654375_create_index_idx_jobs_vehicleid/down.sql create mode 100644 hasura/migrations/1726868654375_create_index_idx_jobs_vehicleid/up.sql diff --git a/hasura/migrations/1726868398933_run_sql_migration/down.sql b/hasura/migrations/1726868398933_run_sql_migration/down.sql new file mode 100644 index 000000000..20f7cb64f --- /dev/null +++ b/hasura/migrations/1726868398933_run_sql_migration/down.sql @@ -0,0 +1,3 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE INDEX idx_jobs_created_at_desc ON jobs (created_at DESC); diff --git a/hasura/migrations/1726868398933_run_sql_migration/up.sql b/hasura/migrations/1726868398933_run_sql_migration/up.sql new file mode 100644 index 000000000..e68cba2a2 --- /dev/null +++ b/hasura/migrations/1726868398933_run_sql_migration/up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_jobs_created_at_desc ON jobs (created_at DESC); diff --git a/hasura/migrations/1726868654375_create_index_idx_jobs_vehicleid/down.sql b/hasura/migrations/1726868654375_create_index_idx_jobs_vehicleid/down.sql new file mode 100644 index 000000000..edec33bc5 --- /dev/null +++ b/hasura/migrations/1726868654375_create_index_idx_jobs_vehicleid/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_vehicleid"; diff --git a/hasura/migrations/1726868654375_create_index_idx_jobs_vehicleid/up.sql b/hasura/migrations/1726868654375_create_index_idx_jobs_vehicleid/up.sql new file mode 100644 index 000000000..0027bbc21 --- /dev/null +++ b/hasura/migrations/1726868654375_create_index_idx_jobs_vehicleid/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_vehicleid" on + "public"."jobs" using btree ("vehicleid"); From da7b97042ea45388792ba8252a71536180270fd1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 20 Sep 2024 15:19:24 -0700 Subject: [PATCH 26/26] IO-2928 Adjust accumulator in reducer for tax Signed-off-by: Allan Carr --- server/accounting/qbo/qbo-payables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 30f5b1f62..c0673cbc5 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -221,7 +221,7 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) Amount: Dinero({ amount: Math.round( bill.billlines.reduce((acc, val) => { - return acc + val.applicable_taxes?.federal ? (val.actual_cost * val.quantity ?? 0) : 0; + return acc + (val.applicable_taxes?.federal ? (val.actual_cost * val.quantity ?? 0) : 0); }, 0) * 100 ) })