From 30cb4ef5620b43a389ac8aa90cafb545062121bf Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 16 Sep 2024 11:06:56 -0700 Subject: [PATCH 001/145] 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 0fcee5b25ed7943f03bd31c4394df82323d18f88 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 16 Sep 2024 12:20:17 -0700 Subject: [PATCH 002/145] IO-2936 Scoreboard AsOfToday Signed-off-by: Allan Carr --- .../scoreboard-targets-table/scoreboard-targets-table.util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js index 47394d966..32703881c 100644 --- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js +++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.util.js @@ -4,7 +4,7 @@ export const CalculateWorkingDaysThisMonth = () => dayjs().endOf("month").busine export const CalculateWorkingDaysInPeriod = (start, end) => dayjs(end).businessDiff(dayjs(start)); -export const CalculateWorkingDaysAsOfToday = () => dayjs().businessDaysInMonth().length; +export const CalculateWorkingDaysAsOfToday = () => dayjs().endOf("day").businessDiff(dayjs().startOf("month")); export const CalculateWorkingDaysLastMonth = () => dayjs().subtract(1, "month").endOf("month").businessDaysInMonth().length; From cbc164dbebd6cb7532c7014fe732429dc4390de6 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 16 Sep 2024 14:33:17 -0700 Subject: [PATCH 003/145] IO-2933 Add in ability to text payments for multiple ROs. --- bodyshop_translations.babel | 23 +- .../card-payment-modal.component..jsx | 85 +- .../payments-generate-link.component.jsx | 32 +- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 7167 +++++++++-------- client/src/translations/fr/common.json | 1 + server/intellipay/intellipay.js | 70 +- 7 files changed, 3736 insertions(+), 3643 deletions(-) diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 3180222bb..3f0edb7b9 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 007/145] 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 008/145] 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 009/145] 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 010/145] 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 011/145] 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 012/145] 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 013/145] 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 014/145] 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 015/145] 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 016/145] 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 017/145] 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 9a0674f5d75ecf05074f183a40d521f692f19634 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 19 Sep 2024 12:58:13 -0700 Subject: [PATCH 018/145] IO-2950 Add subscription using view instead of variable. --- .../production-board-kanban.container.jsx | 17 ++-- .../production-list-table.container.jsx | 9 +- client/src/graphql/jobs.queries.js | 90 +++++++++++++++++++ .../production-board.component.jsx | 24 ++++- .../production-list.component.jsx | 24 ++++- hasura/metadata/tables.yaml | 89 ++++++++++++++++++ .../1726773072213_run_sql_migration/down.sql | 11 +++ .../1726773072213_run_sql_migration/up.sql | 9 ++ .../1726773316245_run_sql_migration/down.sql | 8 ++ .../1726773316245_run_sql_migration/up.sql | 6 ++ .../1726773326353_run_sql_migration/down.sql | 8 ++ .../1726773326353_run_sql_migration/up.sql | 6 ++ .../1726775434726_run_sql_migration/down.sql | 3 + .../1726775434726_run_sql_migration/up.sql | 1 + .../down.sql | 2 + .../up.sql | 1 + 16 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 hasura/migrations/1726773072213_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726773072213_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726773316245_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726773316245_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726773326353_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726773326353_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726775434726_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726775434726_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726775487038_drop_index_idx_jobs_inproduction_true_cast/down.sql create mode 100644 hasura/migrations/1726775487038_drop_index_idx_jobs_inproduction_true_cast/up.sql diff --git a/client/src/components/production-board-kanban/production-board-kanban.container.jsx b/client/src/components/production-board-kanban/production-board-kanban.container.jsx index a0b8cc984..364ee2d53 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.container.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.container.jsx @@ -2,7 +2,11 @@ import React, { useEffect, useMemo } from "react"; import { useQuery, useSubscription } from "@apollo/client"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; -import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries"; +import { + QUERY_JOBS_IN_PRODUCTION, + SUBSCRIPTION_JOBS_IN_PRODUCTION, + SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW +} from "../../graphql/jobs.queries"; import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import ProductionBoardKanbanComponent from "./production-board-kanban.component"; @@ -12,7 +16,7 @@ const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser }); -function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { +function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionType = "direct" }) { const combinedStatuses = useMemo( () => [ ...bodyshop.md_ro_statuses.production_statuses, @@ -28,9 +32,12 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`) }); - const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, { - onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`) - }); + const { data: updatedJobs } = useSubscription( + subscriptionType !== "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION, + { + onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`) + } + ); const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, { variables: { email: currentUser.email }, diff --git a/client/src/components/production-list-table/production-list-table.container.jsx b/client/src/components/production-list-table/production-list-table.container.jsx index 29ddf015a..153089ddb 100644 --- a/client/src/components/production-list-table/production-list-table.container.jsx +++ b/client/src/components/production-list-table/production-list-table.container.jsx @@ -4,12 +4,13 @@ import { QUERY_EXACT_JOB_IN_PRODUCTION, QUERY_EXACT_JOBS_IN_PRODUCTION, QUERY_JOBS_IN_PRODUCTION, - SUBSCRIPTION_JOBS_IN_PRODUCTION + SUBSCRIPTION_JOBS_IN_PRODUCTION, + SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW } from "../../graphql/jobs.queries"; import ProductionListTable from "./production-list-table.component"; import _ from "lodash"; -export default function ProductionListTableContainer() { +export default function ProductionListTableContainer({ subscriptionType = "direct" }) { const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, { pollInterval: 3600000, fetchPolicy: "network-only", @@ -17,7 +18,9 @@ export default function ProductionListTableContainer() { }); const client = useApolloClient(); const [joblist, setJoblist] = useState([]); - const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION); + const { data: updatedJobs } = useSubscription( + subscriptionType !== "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION + ); useEffect(() => { if (!(data && data.jobs)) return; diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 90169eccc..03a1800d3 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -2461,6 +2461,14 @@ export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql` } } `; +export const SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW = gql` + subscription SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW { + jobs: jobs_inproduction { + id + updated_at + } + } +`; export const QUERY_JOBS_IN_PRODUCTION = gql` query QUERY_JOBS_IN_PRODUCTION { @@ -2543,3 +2551,85 @@ export const QUERY_JOBS_IN_PRODUCTION = gql` } } `; + +export const QUERY_JOBS_IN_PRODUCTION_VIEW = gql` + query QUERY_JOBS_IN_PRODUCTION { + jobs: jobs_inproduction { + tasks_aggregate(where: { completed: { _eq: false }, deleted: { _eq: false } }) { + aggregate { + count + } + } + id + updated_at + comment + status + category + iouparent + ro_number + ownerid + ownr_fn + ownr_ln + ownr_co_nm + v_model_yr + v_model_desc + clm_no + v_make_desc + v_color + vehicleid + plate_no + actual_in + scheduled_completion + scheduled_delivery + date_last_contacted + date_next_contact + ins_co_nm + clm_total + ownr_ph1 + ownr_ph2 + special_coverage_policy + owner_owing + production_vars + kanbanparent + alt_transport + employee_body + employee_refinish + employee_prep + employee_csr + est_ct_fn + est_ct_ln + suspended + job_totals + date_repairstarted + joblines_status { + part_type + status + count + } + labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) { + aggregate { + sum { + mod_lb_hrs + } + } + } + larhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _eq: "LAR" } }, { removed: { _eq: false } }] }) { + aggregate { + sum { + mod_lb_hrs + } + } + } + subletLines: joblines( + where: { _and: { part_type: { _in: ["PAS", "PASL"] }, removed: { _eq: false } } } + order_by: { line_no: asc } + ) { + id + line_desc + sublet_ignored + sublet_completed + jobid + } + } + } +`; diff --git a/client/src/pages/production-board/production-board.component.jsx b/client/src/pages/production-board/production-board.component.jsx index 9b69da414..e5dfe7dff 100644 --- a/client/src/pages/production-board/production-board.component.jsx +++ b/client/src/pages/production-board/production-board.component.jsx @@ -1,6 +1,26 @@ import React from "react"; import ProductionBoardKanbanContainer from "../../components/production-board-kanban/production-board-kanban.container"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardComponent); -export default function ProductionBoardComponent() { - return ; +export function ProductionBoardComponent({ bodyshop }) { + const { + treatments: { Production_Use_View } + } = useSplitTreatments({ + attributes: {}, + names: ["Production_Use_View"], + splitKey: bodyshop && bodyshop.imexshopid + }); + + return ; } diff --git a/client/src/pages/production-list/production-list.component.jsx b/client/src/pages/production-list/production-list.component.jsx index 01dd29d51..177108f6d 100644 --- a/client/src/pages/production-list/production-list.component.jsx +++ b/client/src/pages/production-list/production-list.component.jsx @@ -2,11 +2,31 @@ import React from "react"; import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container"; import ProductionListTable from "../../components/production-list-table/production-list-table.container"; -export default function ProductionListComponent() { +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(ProductionListComponent); + +export function ProductionListComponent({ bodyshop }) { + const { + treatments: { Production_Use_View } + } = useSplitTreatments({ + attributes: {}, + names: ["Production_Use_View"], + splitKey: bodyshop && bodyshop.imexshopid + }); + return ( <> - + ); } diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 7ab9621df..70be46ad0 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -918,6 +918,7 @@ - bill_tax_rates - cdk_configuration - cdk_dealerid + - chatterid - city - claimscorpid - convenient_company @@ -939,6 +940,7 @@ - inhousevendorid - insurance_vendor_id - intakechecklist + - intellipay_config - jc_hourly_rates - jobsizelimit - last_name_first @@ -1040,6 +1042,7 @@ - inhousevendorid - insurance_vendor_id - intakechecklist + - intellipay_config - jc_hourly_rates - last_name_first - localmediaserverhttp @@ -4240,6 +4243,63 @@ - active: _eq: true event_triggers: + - name: job_modified + definition: + enable_manual: false + update: + columns: + - clm_no + - v_make_desc + - date_next_contact + - status + - employee_csr + - employee_prep + - clm_total + - suspended + - employee_body + - ro_number + - actual_in + - ownr_co_nm + - v_model_yr + - comment + - job_totals + - v_vin + - ownr_fn + - scheduled_completion + - special_coverage_policy + - v_color + - ca_gst_registrant + - scheduled_delivery + - actual_delivery + - actual_completion + - kanbanparent + - est_ct_fn + - employee_refinish + - ownr_ph1 + - date_last_contacted + - alt_transport + - inproduction + - est_ct_ln + - production_vars + - category + - v_model_desc + - date_invoiced + - est_co_nm + - ownr_ln + retry_conf: + interval_sec: 10 + num_retries: 0 + timeout_sec: 60 + webhook_from_env: HASURA_API_URL + headers: + - name: event-secret + value_from_env: EVENT_SECRET + request_transform: + method: POST + query_params: {} + template_engine: Kriti + url: '{{$base_url}}/job/job-updated' + version: 2 - name: job_status_transition definition: enable_manual: true @@ -4299,6 +4359,35 @@ template_engine: Kriti url: '{{$base_url}}/opensearch' version: 2 +- table: + name: jobs_inproduction + schema: public + object_relationships: + - name: bodyshop + using: + manual_configuration: + column_mapping: + shopid: id + insertion_order: null + remote_table: + name: bodyshops + schema: public + select_permissions: + - role: user + permission: + columns: + - id + - shopid + - updated_at + filter: + bodyshop: + associations: + _and: + - user: + authid: + _eq: X-Hasura-User-Id + - active: + _eq: true - table: name: masterdata schema: public diff --git a/hasura/migrations/1726773072213_run_sql_migration/down.sql b/hasura/migrations/1726773072213_run_sql_migration/down.sql new file mode 100644 index 000000000..4f5681185 --- /dev/null +++ b/hasura/migrations/1726773072213_run_sql_migration/down.sql @@ -0,0 +1,11 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE +-- OR REPLACE VIEW "public"."jobs_inproduction" AS +-- SELECT +-- j.id, +-- j.updated_at +-- FROM +-- jobs j +-- WHERE +-- j.inproduction=true; diff --git a/hasura/migrations/1726773072213_run_sql_migration/up.sql b/hasura/migrations/1726773072213_run_sql_migration/up.sql new file mode 100644 index 000000000..317afef83 --- /dev/null +++ b/hasura/migrations/1726773072213_run_sql_migration/up.sql @@ -0,0 +1,9 @@ +CREATE +OR REPLACE VIEW "public"."jobs_inproduction" AS +SELECT + j.id, + j.updated_at +FROM + jobs j +WHERE +j.inproduction=true; diff --git a/hasura/migrations/1726773316245_run_sql_migration/down.sql b/hasura/migrations/1726773316245_run_sql_migration/down.sql new file mode 100644 index 000000000..153bb816b --- /dev/null +++ b/hasura/migrations/1726773316245_run_sql_migration/down.sql @@ -0,0 +1,8 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS +-- SELECT j.id, +-- j.updated_at, +-- j.shopid +-- FROM jobs j +-- WHERE (j.inproduction = true); diff --git a/hasura/migrations/1726773316245_run_sql_migration/up.sql b/hasura/migrations/1726773316245_run_sql_migration/up.sql new file mode 100644 index 000000000..8bc73f558 --- /dev/null +++ b/hasura/migrations/1726773316245_run_sql_migration/up.sql @@ -0,0 +1,6 @@ +CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS + SELECT j.id, + j.updated_at, + j.shopid + FROM jobs j + WHERE (j.inproduction = true); diff --git a/hasura/migrations/1726773326353_run_sql_migration/down.sql b/hasura/migrations/1726773326353_run_sql_migration/down.sql new file mode 100644 index 000000000..153bb816b --- /dev/null +++ b/hasura/migrations/1726773326353_run_sql_migration/down.sql @@ -0,0 +1,8 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS +-- SELECT j.id, +-- j.updated_at, +-- j.shopid +-- FROM jobs j +-- WHERE (j.inproduction = true); diff --git a/hasura/migrations/1726773326353_run_sql_migration/up.sql b/hasura/migrations/1726773326353_run_sql_migration/up.sql new file mode 100644 index 000000000..8bc73f558 --- /dev/null +++ b/hasura/migrations/1726773326353_run_sql_migration/up.sql @@ -0,0 +1,6 @@ +CREATE OR REPLACE VIEW "public"."jobs_inproduction" AS + SELECT j.id, + j.updated_at, + j.shopid + FROM jobs j + WHERE (j.inproduction = true); diff --git a/hasura/migrations/1726775434726_run_sql_migration/down.sql b/hasura/migrations/1726775434726_run_sql_migration/down.sql new file mode 100644 index 000000000..16ca01c42 --- /dev/null +++ b/hasura/migrations/1726775434726_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_inproduction_true_cast ON jobs(inproduction) WHERE inproduction = ('true') :: boolean; diff --git a/hasura/migrations/1726775434726_run_sql_migration/up.sql b/hasura/migrations/1726775434726_run_sql_migration/up.sql new file mode 100644 index 000000000..a6f352908 --- /dev/null +++ b/hasura/migrations/1726775434726_run_sql_migration/up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_jobs_inproduction_true_cast ON jobs(inproduction) WHERE inproduction = ('true') :: boolean; diff --git a/hasura/migrations/1726775487038_drop_index_idx_jobs_inproduction_true_cast/down.sql b/hasura/migrations/1726775487038_drop_index_idx_jobs_inproduction_true_cast/down.sql new file mode 100644 index 000000000..a5f6e49ae --- /dev/null +++ b/hasura/migrations/1726775487038_drop_index_idx_jobs_inproduction_true_cast/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_jobs_inproduction_true_cast" on + "public"."jobs" using btree ("inproduction"); diff --git a/hasura/migrations/1726775487038_drop_index_idx_jobs_inproduction_true_cast/up.sql b/hasura/migrations/1726775487038_drop_index_idx_jobs_inproduction_true_cast/up.sql new file mode 100644 index 000000000..d0163192a --- /dev/null +++ b/hasura/migrations/1726775487038_drop_index_idx_jobs_inproduction_true_cast/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_jobs_inproduction_true_cast"; From f7af3b407b03776b4baf822521e0cc0fba0b112a Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 19 Sep 2024 16:06:59 -0400 Subject: [PATCH 019/145] clear stage Signed-off-by: Dave Richer --- .../production-board-kanban.container.jsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/client/src/components/production-board-kanban/production-board-kanban.container.jsx b/client/src/components/production-board-kanban/production-board-kanban.container.jsx index a0b8cc984..d94cb4e42 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.container.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.container.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from "react"; +import React, { useEffect, useMemo, useRef } from "react"; import { useQuery, useSubscription } from "@apollo/client"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -13,6 +13,8 @@ const mapStateToProps = createStructuredSelector({ }); function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { + const fired = useRef(false); // useRef to keep track of whether the subscription fired + const combinedStatuses = useMemo( () => [ ...bodyshop.md_ro_statuses.production_statuses, @@ -40,10 +42,15 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) { // const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {})); useEffect(() => { - if (updatedJobs && data) { - refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`)); + if (!updatedJobs) { + return; } - }, [updatedJobs, data, refetch]); + if (!fired.current) { + fired.current = true; + return; + } + refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`)); + }, [updatedJobs, refetch]); const filteredAssociationSettings = useMemo(() => { return associationSettings?.associations[0] || null; From 64928d0849ffd6e0833e40b4989cdfc236d8e17f Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 19 Sep 2024 13:11:31 -0700 Subject: [PATCH 020/145] IO-2950 remove unneeded updated query. --- client/src/graphql/jobs.queries.js | 82 ------------------------------ 1 file changed, 82 deletions(-) diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 03a1800d3..a71d08d10 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -2551,85 +2551,3 @@ export const QUERY_JOBS_IN_PRODUCTION = gql` } } `; - -export const QUERY_JOBS_IN_PRODUCTION_VIEW = gql` - query QUERY_JOBS_IN_PRODUCTION { - jobs: jobs_inproduction { - tasks_aggregate(where: { completed: { _eq: false }, deleted: { _eq: false } }) { - aggregate { - count - } - } - id - updated_at - comment - status - category - iouparent - ro_number - ownerid - ownr_fn - ownr_ln - ownr_co_nm - v_model_yr - v_model_desc - clm_no - v_make_desc - v_color - vehicleid - plate_no - actual_in - scheduled_completion - scheduled_delivery - date_last_contacted - date_next_contact - ins_co_nm - clm_total - ownr_ph1 - ownr_ph2 - special_coverage_policy - owner_owing - production_vars - kanbanparent - alt_transport - employee_body - employee_refinish - employee_prep - employee_csr - est_ct_fn - est_ct_ln - suspended - job_totals - date_repairstarted - joblines_status { - part_type - status - count - } - labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) { - aggregate { - sum { - mod_lb_hrs - } - } - } - larhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _eq: "LAR" } }, { removed: { _eq: false } }] }) { - aggregate { - sum { - mod_lb_hrs - } - } - } - subletLines: joblines( - where: { _and: { part_type: { _in: ["PAS", "PASL"] }, removed: { _eq: false } } } - order_by: { line_no: asc } - ) { - id - line_desc - sublet_ignored - sublet_completed - jobid - } - } - } -`; From fc6ec54233a40cb8164557c8a35b58b9f5911fd9 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 19 Sep 2024 13:18:35 -0700 Subject: [PATCH 021/145] IO-2950 Resolve reversed split. --- .../production-board-kanban.container.jsx | 2 +- .../production-list-table/production-list-table.container.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/production-board-kanban/production-board-kanban.container.jsx b/client/src/components/production-board-kanban/production-board-kanban.container.jsx index 240c1cd6f..45bfb353c 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.container.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.container.jsx @@ -35,7 +35,7 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser, subscriptionTyp }); const { data: updatedJobs } = useSubscription( - subscriptionType !== "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION, + subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION, { onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`) } diff --git a/client/src/components/production-list-table/production-list-table.container.jsx b/client/src/components/production-list-table/production-list-table.container.jsx index 153089ddb..9f564bde7 100644 --- a/client/src/components/production-list-table/production-list-table.container.jsx +++ b/client/src/components/production-list-table/production-list-table.container.jsx @@ -19,7 +19,7 @@ export default function ProductionListTableContainer({ subscriptionType = "direc const client = useApolloClient(); const [joblist, setJoblist] = useState([]); const { data: updatedJobs } = useSubscription( - subscriptionType !== "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION + subscriptionType === "view" ? SUBSCRIPTION_JOBS_IN_PRODUCTION_VIEW : SUBSCRIPTION_JOBS_IN_PRODUCTION ); useEffect(() => { From 4c0a1960ad10b3d050830798e20fb981a30910ee Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 20 Sep 2024 09:37:09 -0700 Subject: [PATCH 022/145] 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 023/145] 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 024/145] 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 025/145] 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 026/145] 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 027/145] 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 028/145] 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 ) }) From efc1157653cbb4e3bc03d97bb2a175dc8ec21ee3 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 20 Sep 2024 18:53:33 -0400 Subject: [PATCH 029/145] IO-2782 - Fix query Signed-off-by: Dave Richer --- server/firebase/firebase-handler.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/firebase/firebase-handler.js b/server/firebase/firebase-handler.js index e47eeffc1..2590e6f57 100644 --- a/server/firebase/firebase-handler.js +++ b/server/firebase/firebase-handler.js @@ -214,7 +214,6 @@ const getUser = (req, res) => { query GET_USER_BY_AUTHID($authid: String!) { users(where: { authid: { _eq: $authid } }) { email - displayName validemail associations { id From 464f7044f0dd3aadb18c770213a77bb16488cc5a Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 20 Sep 2024 15:56:42 -0700 Subject: [PATCH 030/145] Further index changes. --- .../1726871373784_run_sql_migration/down.sql | 34 +++++++++++++++++++ .../1726871373784_run_sql_migration/up.sql | 32 +++++++++++++++++ .../1726871384601_run_sql_migration/down.sql | 34 +++++++++++++++++++ .../1726871384601_run_sql_migration/up.sql | 32 +++++++++++++++++ .../down.sql | 1 + .../up.sql | 2 ++ .../down.sql | 1 + .../up.sql | 2 ++ .../down.sql | 1 + .../up.sql | 2 ++ .../down.sql | 1 + .../up.sql | 2 ++ .../1726872195945_run_sql_migration/down.sql | 3 ++ .../1726872195945_run_sql_migration/up.sql | 1 + .../1726872572029_run_sql_migration/down.sql | 3 ++ .../1726872572029_run_sql_migration/up.sql | 1 + .../1726872872872_run_sql_migration/down.sql | 3 ++ .../1726872872872_run_sql_migration/up.sql | 1 + .../down.sql | 2 ++ .../up.sql | 1 + .../down.sql | 2 ++ .../up.sql | 1 + 22 files changed, 162 insertions(+) create mode 100644 hasura/migrations/1726871373784_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726871373784_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726871384601_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726871384601_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726871425044_create_index_idx_bill_invoice_number/down.sql create mode 100644 hasura/migrations/1726871425044_create_index_idx_bill_invoice_number/up.sql create mode 100644 hasura/migrations/1726871466395_create_index_idx_payments_paymentnum/down.sql create mode 100644 hasura/migrations/1726871466395_create_index_idx_payments_paymentnum/up.sql create mode 100644 hasura/migrations/1726871529961_create_index_exportlog_useremail/down.sql create mode 100644 hasura/migrations/1726871529961_create_index_exportlog_useremail/up.sql create mode 100644 hasura/migrations/1726871632920_create_index_available_jobs_jobid/down.sql create mode 100644 hasura/migrations/1726871632920_create_index_available_jobs_jobid/up.sql create mode 100644 hasura/migrations/1726872195945_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726872195945_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726872572029_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726872572029_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726872872872_run_sql_migration/down.sql create mode 100644 hasura/migrations/1726872872872_run_sql_migration/up.sql create mode 100644 hasura/migrations/1726872925447_drop_index_joblines_idx_removed/down.sql create mode 100644 hasura/migrations/1726872925447_drop_index_joblines_idx_removed/up.sql create mode 100644 hasura/migrations/1726872944447_drop_index_joblines_idx_line_no/down.sql create mode 100644 hasura/migrations/1726872944447_drop_index_joblines_idx_line_no/up.sql diff --git a/hasura/migrations/1726871373784_run_sql_migration/down.sql b/hasura/migrations/1726871373784_run_sql_migration/down.sql new file mode 100644 index 000000000..9268ff591 --- /dev/null +++ b/hasura/migrations/1726871373784_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.search_exportlog(search text) +-- RETURNS SETOF exportlog +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ BEGIN IF search = '' THEN RETURN query +-- SELECT +-- * +-- FROM +-- exportlog e; +-- ELSE RETURN query +-- SELECT +-- e.* +-- FROM +-- exportlog e +-- LEFT JOIN jobs j on j.id = e.jobid +-- LEFT JOIN payments p +-- ON p.id = e.paymentid +-- LEFT JOIN bills b +-- ON e.billid = b.id +-- WHERE +-- ( +-- j.ro_number ILIKE '%' || search +-- OR b.invoice_number ILIKE '%' || search +-- OR p.paymentnum ILIKE '%' || search +-- OR e.useremail ILIKE '%' || search +-- ) +-- AND (e.jobid = j.id +-- or e.paymentid = p.id +-- or e.billid = b.id) +-- ; +-- END IF; +-- END $function$; diff --git a/hasura/migrations/1726871373784_run_sql_migration/up.sql b/hasura/migrations/1726871373784_run_sql_migration/up.sql new file mode 100644 index 000000000..eb8c0b69d --- /dev/null +++ b/hasura/migrations/1726871373784_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.search_exportlog(search text) + RETURNS SETOF exportlog + LANGUAGE plpgsql + STABLE +AS $function$ BEGIN IF search = '' THEN RETURN query +SELECT + * +FROM + exportlog e; + ELSE RETURN query +SELECT + e.* +FROM + exportlog e + LEFT JOIN jobs j on j.id = e.jobid +LEFT JOIN payments p + ON p.id = e.paymentid +LEFT JOIN bills b + ON e.billid = b.id +WHERE + ( + j.ro_number ILIKE '%' || search + OR b.invoice_number ILIKE '%' || search + OR p.paymentnum ILIKE '%' || search + OR e.useremail ILIKE '%' || search + ) + AND (e.jobid = j.id + or e.paymentid = p.id + or e.billid = b.id) +; +END IF; +END $function$; diff --git a/hasura/migrations/1726871384601_run_sql_migration/down.sql b/hasura/migrations/1726871384601_run_sql_migration/down.sql new file mode 100644 index 000000000..9268ff591 --- /dev/null +++ b/hasura/migrations/1726871384601_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.search_exportlog(search text) +-- RETURNS SETOF exportlog +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ BEGIN IF search = '' THEN RETURN query +-- SELECT +-- * +-- FROM +-- exportlog e; +-- ELSE RETURN query +-- SELECT +-- e.* +-- FROM +-- exportlog e +-- LEFT JOIN jobs j on j.id = e.jobid +-- LEFT JOIN payments p +-- ON p.id = e.paymentid +-- LEFT JOIN bills b +-- ON e.billid = b.id +-- WHERE +-- ( +-- j.ro_number ILIKE '%' || search +-- OR b.invoice_number ILIKE '%' || search +-- OR p.paymentnum ILIKE '%' || search +-- OR e.useremail ILIKE '%' || search +-- ) +-- AND (e.jobid = j.id +-- or e.paymentid = p.id +-- or e.billid = b.id) +-- ; +-- END IF; +-- END $function$; diff --git a/hasura/migrations/1726871384601_run_sql_migration/up.sql b/hasura/migrations/1726871384601_run_sql_migration/up.sql new file mode 100644 index 000000000..eb8c0b69d --- /dev/null +++ b/hasura/migrations/1726871384601_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.search_exportlog(search text) + RETURNS SETOF exportlog + LANGUAGE plpgsql + STABLE +AS $function$ BEGIN IF search = '' THEN RETURN query +SELECT + * +FROM + exportlog e; + ELSE RETURN query +SELECT + e.* +FROM + exportlog e + LEFT JOIN jobs j on j.id = e.jobid +LEFT JOIN payments p + ON p.id = e.paymentid +LEFT JOIN bills b + ON e.billid = b.id +WHERE + ( + j.ro_number ILIKE '%' || search + OR b.invoice_number ILIKE '%' || search + OR p.paymentnum ILIKE '%' || search + OR e.useremail ILIKE '%' || search + ) + AND (e.jobid = j.id + or e.paymentid = p.id + or e.billid = b.id) +; +END IF; +END $function$; diff --git a/hasura/migrations/1726871425044_create_index_idx_bill_invoice_number/down.sql b/hasura/migrations/1726871425044_create_index_idx_bill_invoice_number/down.sql new file mode 100644 index 000000000..ba25d24f4 --- /dev/null +++ b/hasura/migrations/1726871425044_create_index_idx_bill_invoice_number/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_bill_invoice_number"; diff --git a/hasura/migrations/1726871425044_create_index_idx_bill_invoice_number/up.sql b/hasura/migrations/1726871425044_create_index_idx_bill_invoice_number/up.sql new file mode 100644 index 000000000..9048c0824 --- /dev/null +++ b/hasura/migrations/1726871425044_create_index_idx_bill_invoice_number/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_bill_invoice_number" on + "public"."bills" using btree ("invoice_number"); diff --git a/hasura/migrations/1726871466395_create_index_idx_payments_paymentnum/down.sql b/hasura/migrations/1726871466395_create_index_idx_payments_paymentnum/down.sql new file mode 100644 index 000000000..7e7d6a7e3 --- /dev/null +++ b/hasura/migrations/1726871466395_create_index_idx_payments_paymentnum/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."idx_payments_paymentnum"; diff --git a/hasura/migrations/1726871466395_create_index_idx_payments_paymentnum/up.sql b/hasura/migrations/1726871466395_create_index_idx_payments_paymentnum/up.sql new file mode 100644 index 000000000..e5df914da --- /dev/null +++ b/hasura/migrations/1726871466395_create_index_idx_payments_paymentnum/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "idx_payments_paymentnum" on + "public"."payments" using btree ("paymentnum"); diff --git a/hasura/migrations/1726871529961_create_index_exportlog_useremail/down.sql b/hasura/migrations/1726871529961_create_index_exportlog_useremail/down.sql new file mode 100644 index 000000000..a2c72a3b7 --- /dev/null +++ b/hasura/migrations/1726871529961_create_index_exportlog_useremail/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."exportlog_useremail"; diff --git a/hasura/migrations/1726871529961_create_index_exportlog_useremail/up.sql b/hasura/migrations/1726871529961_create_index_exportlog_useremail/up.sql new file mode 100644 index 000000000..d5b7f3c91 --- /dev/null +++ b/hasura/migrations/1726871529961_create_index_exportlog_useremail/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "exportlog_useremail" on + "public"."exportlog" using btree ("useremail"); diff --git a/hasura/migrations/1726871632920_create_index_available_jobs_jobid/down.sql b/hasura/migrations/1726871632920_create_index_available_jobs_jobid/down.sql new file mode 100644 index 000000000..ef5073c55 --- /dev/null +++ b/hasura/migrations/1726871632920_create_index_available_jobs_jobid/down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."available_jobs_jobid"; diff --git a/hasura/migrations/1726871632920_create_index_available_jobs_jobid/up.sql b/hasura/migrations/1726871632920_create_index_available_jobs_jobid/up.sql new file mode 100644 index 000000000..2f6285f05 --- /dev/null +++ b/hasura/migrations/1726871632920_create_index_available_jobs_jobid/up.sql @@ -0,0 +1,2 @@ +CREATE INDEX "available_jobs_jobid" on + "public"."available_jobs" using btree ("jobid"); diff --git a/hasura/migrations/1726872195945_run_sql_migration/down.sql b/hasura/migrations/1726872195945_run_sql_migration/down.sql new file mode 100644 index 000000000..ef22d98bc --- /dev/null +++ b/hasura/migrations/1726872195945_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_jobslines_ordering ON joblines (jobid, removed, line_no asc) where removed=false; diff --git a/hasura/migrations/1726872195945_run_sql_migration/up.sql b/hasura/migrations/1726872195945_run_sql_migration/up.sql new file mode 100644 index 000000000..77ed0f842 --- /dev/null +++ b/hasura/migrations/1726872195945_run_sql_migration/up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_jobslines_ordering ON joblines (jobid, removed, line_no asc) where removed=false; diff --git a/hasura/migrations/1726872572029_run_sql_migration/down.sql b/hasura/migrations/1726872572029_run_sql_migration/down.sql new file mode 100644 index 000000000..d1c5e8254 --- /dev/null +++ b/hasura/migrations/1726872572029_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_joblines_types ON joblines (jobid, mod_lbr_ty, removed) where removed=false; diff --git a/hasura/migrations/1726872572029_run_sql_migration/up.sql b/hasura/migrations/1726872572029_run_sql_migration/up.sql new file mode 100644 index 000000000..e1ddd1a7f --- /dev/null +++ b/hasura/migrations/1726872572029_run_sql_migration/up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_joblines_types ON joblines (jobid, mod_lbr_ty, removed) where removed=false; diff --git a/hasura/migrations/1726872872872_run_sql_migration/down.sql b/hasura/migrations/1726872872872_run_sql_migration/down.sql new file mode 100644 index 000000000..9fe48a789 --- /dev/null +++ b/hasura/migrations/1726872872872_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_exportlog_created_at_desc ON exportlog (created_at desc); diff --git a/hasura/migrations/1726872872872_run_sql_migration/up.sql b/hasura/migrations/1726872872872_run_sql_migration/up.sql new file mode 100644 index 000000000..71dae5805 --- /dev/null +++ b/hasura/migrations/1726872872872_run_sql_migration/up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_exportlog_created_at_desc ON exportlog (created_at desc); diff --git a/hasura/migrations/1726872925447_drop_index_joblines_idx_removed/down.sql b/hasura/migrations/1726872925447_drop_index_joblines_idx_removed/down.sql new file mode 100644 index 000000000..dae8a4574 --- /dev/null +++ b/hasura/migrations/1726872925447_drop_index_joblines_idx_removed/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "joblines_idx_removed" on + "public"."joblines" using btree ("removed"); diff --git a/hasura/migrations/1726872925447_drop_index_joblines_idx_removed/up.sql b/hasura/migrations/1726872925447_drop_index_joblines_idx_removed/up.sql new file mode 100644 index 000000000..8ede82ff5 --- /dev/null +++ b/hasura/migrations/1726872925447_drop_index_joblines_idx_removed/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."joblines_idx_removed"; diff --git a/hasura/migrations/1726872944447_drop_index_joblines_idx_line_no/down.sql b/hasura/migrations/1726872944447_drop_index_joblines_idx_line_no/down.sql new file mode 100644 index 000000000..a55c69d5b --- /dev/null +++ b/hasura/migrations/1726872944447_drop_index_joblines_idx_line_no/down.sql @@ -0,0 +1,2 @@ +CREATE INDEX "joblines_idx_line_no" on + "public"."joblines" using btree ("jobid", "line_no"); diff --git a/hasura/migrations/1726872944447_drop_index_joblines_idx_line_no/up.sql b/hasura/migrations/1726872944447_drop_index_joblines_idx_line_no/up.sql new file mode 100644 index 000000000..a07804c70 --- /dev/null +++ b/hasura/migrations/1726872944447_drop_index_joblines_idx_line_no/up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "public"."joblines_idx_line_no"; From 646754732dd9bf0dc33be2af8c674aafad19f100 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 20 Sep 2024 16:45:48 -0700 Subject: [PATCH 031/145] IO-2782 Adjust to Object for items Signed-off-by: Allan Carr --- server/email/sendemail.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/email/sendemail.js b/server/email/sendemail.js index 65b5a60ce..5dea4a075 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -96,7 +96,7 @@ const sendServerEmail = async ({ subject, text }) => { } }; -const sendProManagerWelcomeEmail = async (to, subject, html) => { +const sendProManagerWelcomeEmail = async ({to, subject, html}) => { try { await transporter.sendMail({ from: `ProManager `, From 6a7005299a8e9c70fa712421d0462f6dc9380b0f Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 23 Sep 2024 16:41:06 -0400 Subject: [PATCH 032/145] IO-2931-Visual-Production-Board-Drag-and-Drop-on-Touch-Devices - Fix Drag and Drop on Android and IOS devices. Signed-off-by: Dave Richer --- .../production-board-kanban.styles.scss | 6 +++++- .../find-closest-draggable-id-from-event.js | 2 +- .../sensors/use-touch-sensor.js | 13 +++++++----- .../use-sensor-marshal/use-sensor-marshal.js | 20 ++++++++++++++++--- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/client/src/components/production-board-kanban/production-board-kanban.styles.scss b/client/src/components/production-board-kanban/production-board-kanban.styles.scss index 6e7ee63ce..8b04aaa5d 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.styles.scss +++ b/client/src/components/production-board-kanban/production-board-kanban.styles.scss @@ -17,7 +17,6 @@ border-radius: 5px 5px 0 0; } - .production-alert { background: transparent; border: none; @@ -70,3 +69,8 @@ } } } + +.clone.is-dragging .ant-card { + border: #1890ff 2px solid !important; + border-radius: 12px; +} diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/find-closest-draggable-id-from-event.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/find-closest-draggable-id-from-event.js index 60285d2a4..f17be1759 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/find-closest-draggable-id-from-event.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/find-closest-draggable-id-from-event.js @@ -8,7 +8,7 @@ function getSelector(contextId) { return `[${attributes.dragHandle.contextId}="${contextId}"]`; } -function findClosestDragHandleFromEvent(contextId, event) { +export function findClosestDragHandleFromEvent(contextId, event) { const target = event.target; if (!isElement(target)) { warning("event.target must be a Element"); diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/sensors/use-touch-sensor.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/sensors/use-touch-sensor.js index 3210e2188..d679f7442 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/sensors/use-touch-sensor.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/sensors/use-touch-sensor.js @@ -240,11 +240,14 @@ export default function useTouchSensor(api) { y: clientY }; + const handle = api.findClosestDragHandle(event); + invariant(handle, "Touch sensor unable to find drag handle"); + // unbind this event handler unbindEventsRef.current(); // eslint-disable-next-line no-use-before-define - startPendingDrag(actions, point); + startPendingDrag(actions, point, handle); } }), // not including stop or startPendingDrag as it is not defined initially @@ -288,7 +291,7 @@ export default function useTouchSensor(api) { } }, [stop]); const bindCapturingEvents = useCallback( - function bindCapturingEvents() { + function bindCapturingEvents(target) { const options = { capture: true, passive: false @@ -307,7 +310,7 @@ export default function useTouchSensor(api) { // Old behaviour: // https://gist.github.com/parris/dda613e3ae78f14eb2dc9fa0f4bfce3d // https://stackoverflow.com/questions/33298828/touch-move-event-dont-fire-after-touch-start-target-is-removed - const unbindTarget = bindEvents(window, getHandleBindings(args), options); + const unbindTarget = bindEvents(target, getHandleBindings(args), options); const unbindWindow = bindEvents(window, getWindowBindings(args), options); unbindEventsRef.current = function unbindAll() { unbindTarget(); @@ -330,7 +333,7 @@ export default function useTouchSensor(api) { [getPhase, setPhase] ); const startPendingDrag = useCallback( - function startPendingDrag(actions, point) { + function startPendingDrag(actions, point, target) { invariant(getPhase().type === "IDLE", "Expected to move from IDLE to PENDING drag"); const longPressTimerId = setTimeout(startDragging, timeForLongPress); setPhase({ @@ -339,7 +342,7 @@ export default function useTouchSensor(api) { actions, longPressTimerId }); - bindCapturingEvents(); + bindCapturingEvents(target); }, [bindCapturingEvents, getPhase, setPhase, startDragging] ); diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/use-sensor-marshal.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/use-sensor-marshal.js index 17f84ee6b..c559a56ab 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/use-sensor-marshal.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-sensor-marshal/use-sensor-marshal.js @@ -23,7 +23,9 @@ import getBorderBoxCenterPosition from "../get-border-box-center-position"; import { warning } from "../../dev-warning"; import useLayoutEffect from "../use-isomorphic-layout-effect"; import { noop } from "../../empty"; -import findClosestDraggableIdFromEvent from "./find-closest-draggable-id-from-event"; +import findClosestDraggableIdFromEvent, { + findClosestDragHandleFromEvent +} from "./find-closest-draggable-id-from-event"; import findDraggable from "../get-elements/find-draggable"; import bindEvents from "../event-bindings/bind-events"; @@ -339,6 +341,9 @@ export default function useSensorMarshal({ contextId, store, registry, customSen }), [contextId, lockAPI, registry, store] ); + + const findClosestDragHandle = useCallback((event) => findClosestDragHandleFromEvent(contextId, event), [contextId]); + const findClosestDraggableId = useCallback((event) => findClosestDraggableIdFromEvent(contextId, event), [contextId]); const findOptionsForDraggable = useCallback( (id) => { @@ -370,9 +375,18 @@ export default function useSensorMarshal({ contextId, store, registry, customSen findClosestDraggableId, findOptionsForDraggable, tryReleaseLock, - isLockClaimed + isLockClaimed, + findClosestDragHandle }), - [canGetLock, tryGetLock, findClosestDraggableId, findOptionsForDraggable, tryReleaseLock, isLockClaimed] + [ + canGetLock, + tryGetLock, + findClosestDraggableId, + findOptionsForDraggable, + tryReleaseLock, + isLockClaimed, + findClosestDragHandle + ] ); // Bad ass From 3c85de3e34d0eb9ba35928b030a9bc6498111d0c Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 23 Sep 2024 15:04:51 -0700 Subject: [PATCH 033/145] IO-2945 Resolve no success being sent to intellipay creating triple posting. --- server/intellipay/intellipay.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index f798bc0bd..60319ed29 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -200,7 +200,7 @@ exports.postback = async (req, res) => { } })) }); - logger.log("intellipay-postback-app-success", "DEBUG", req.user?.email, null, { + logger.log("intellipay-postback-app-success", "DEBUG", req.user?.email, JSON.stringify(jobs), { iprequest: values, paymentResult }); @@ -223,8 +223,8 @@ exports.postback = async (req, res) => { .join("
") }) }); - res.sendStatus(200); } + res.sendStatus(200); } else if (values.invoice) { //This is a link email that's been sent out. const job = await gqlClient.request(queries.GET_JOB_BY_PK, { @@ -255,7 +255,7 @@ exports.postback = async (req, res) => { } }); - logger.log("intellipay-postback-link-success", "DEBUG", req.user?.email, null, { + logger.log("intellipay-postback-link-success", "DEBUG", req.user?.email, values.invoice, { iprequest: values, responseResults, paymentResult @@ -263,7 +263,7 @@ exports.postback = async (req, res) => { res.sendStatus(200); } } catch (error) { - logger.log("intellipay-postback-error", "ERROR", req.user?.email, null, { + logger.log("intellipay-postback-total-error", "ERROR", req.user?.email, null, { error: JSON.stringify(error), body: req.body }); From 4dffbfe6fafd55e6a5b75629386a7283a2ece360 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 23 Sep 2024 19:26:40 -0400 Subject: [PATCH 034/145] IO-2931-Visual-Production-Board-Drag-and-Drop-on-Touch-Devices - Add additional fixes / optimizations, from parent repo Signed-off-by: Dave Richer --- .../trello-board/dnd/lib/state/get-droppable-over.js | 4 ++-- .../use-droppable-publisher/get-closest-scrollable.js | 3 ++- .../dnd/lib/view/use-style-marshal/get-styles.js | 8 +++++++- .../dnd/lib/view/use-style-marshal/use-style-marshal.js | 4 +++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/state/get-droppable-over.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/state/get-droppable-over.js index dcf0cf04c..cb7f7c75b 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/state/get-droppable-over.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/state/get-droppable-over.js @@ -25,8 +25,8 @@ function getFurthestAway({ pageBorderBox, draggable, candidates }) { const axis = candidate.axis; const target = patch( candidate.axis.line, - // use the current center of the dragging item on the main axis - pageBorderBox.center[axis.line], + // use the center of the list on the main axis + candidate.page.borderBox.center[axis.line], // use the center of the list on the cross axis candidate.page.borderBox.center[axis.crossAxisLine] ); diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-droppable-publisher/get-closest-scrollable.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-droppable-publisher/get-closest-scrollable.js index 923622788..0b069cce5 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-droppable-publisher/get-closest-scrollable.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-droppable-publisher/get-closest-scrollable.js @@ -5,6 +5,7 @@ import getBodyElement from "../get-body-element"; const isEqual = (base) => (value) => base === value; const isScroll = isEqual("scroll"); const isAuto = isEqual("auto"); +const isOverlay = isEqual("overlay"); const isVisible = isEqual("visible"); const isEither = (overflow, fn) => fn(overflow.overflowX) || fn(overflow.overflowY); const isBoth = (overflow, fn) => fn(overflow.overflowX) && fn(overflow.overflowY); @@ -14,7 +15,7 @@ const isElementScrollable = (el) => { overflowX: style.overflowX, overflowY: style.overflowY }; - return isEither(overflow, isScroll) || isEither(overflow, isAuto); + return isEither(overflow, isScroll) || isEither(overflow, isAuto) || isEither(overflow, isOverlay); }; // Special case for a body element diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/get-styles.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/get-styles.js index 418acb50b..f62947525 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/get-styles.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/get-styles.js @@ -83,7 +83,13 @@ const getFinalStyles = (contextId) => { return { selector: getSelector(attributes.draggable.contextId), styles: { - dragging: transition, + dragging: ` + ${transition} + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + `, dropAnimating: transition, userCancel: transition } diff --git a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/use-style-marshal.js b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/use-style-marshal.js index 19457b312..b27b4d534 100644 --- a/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/use-style-marshal.js +++ b/client/src/components/production-board-kanban/trello-board/dnd/lib/view/use-style-marshal/use-style-marshal.js @@ -67,7 +67,9 @@ export default function useStyleMarshal(contextId, nonce) { const remove = (ref) => { const current = ref.current; invariant(current, "Cannot unmount ref as it is not set"); - getHead().removeChild(current); + if (getHead().contains(current)) { + getHead().removeChild(current); + } ref.current = null; }; remove(alwaysRef); From cafca35500dde29d69c0d392e134983b2e46a7c9 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 23 Sep 2024 18:02:17 -0700 Subject: [PATCH 035/145] IO-2957 Resolve task reminders not sending with incorrect references. --- server/email/tasksEmails.js | 12 +++++----- server/intellipay/intellipay.js | 40 ++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/server/email/tasksEmails.js b/server/email/tasksEmails.js index a106085f7..1f508be31 100644 --- a/server/email/tasksEmails.js +++ b/server/email/tasksEmails.js @@ -95,11 +95,11 @@ const formatPriority = (priority) => { * @returns {{header, body: string, subHeader: string}} */ -const getEndpoints = () => +const getEndpoints = (bodyshop) => InstanceManager({ imex: process.env?.NODE_ENV === "test" ? "https://test.imex.online" : "https://imex.online", rome: - bodyshop.convenient_company === "promanager" + bodyshop?.convenient_company === "promanager" ? process.env?.NODE_ENV === "test" ? "https//test.promanager.web-est.com" : "https://promanager.web-est.com" @@ -109,7 +109,7 @@ const getEndpoints = () => }); const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, job, taskId) => { - const endPoints = getEndpoints(); + const endPoints = getEndpoints(bodyshop); return { header: title, subHeader: `Body Shop: ${bodyshop.shopname} | Priority: ${formatPriority(priority)} ${formatDate(dueDate)}`, @@ -247,7 +247,7 @@ const tasksRemindEmail = async (req, res) => { const fromEmails = InstanceManager({ imex: "ImEX Online ", rome: - onlyTask.bodyshop.convenient_company === "promanager" + tasksRequest?.tasks[0].bodyshop.convenient_company === "promanager" ? "ProManager " : "Rome Online " }); @@ -283,7 +283,7 @@ const tasksRemindEmail = async (req, res) => { const endPoints = InstanceManager({ imex: process.env?.NODE_ENV === "test" ? "https://test.imex.online" : "https://imex.online", rome: - allTasks[0].bodyshop.convenient_company === "promanager" + tasksRequest?.tasks[0].bodyshop.convenient_company === "promanager" ? process.env?.NODE_ENV === "test" ? "https//test.promanager.web-est.com" : "https://promanager.web-est.com" @@ -320,7 +320,7 @@ const tasksRemindEmail = async (req, res) => { tasksEmailQueue.push(taskId); } }, - allTasks[0].bodyshop.convenient_company + tasksRequest?.tasks[0].bodyshop.convenient_company ); } }); diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index 60319ed29..3c6289b9b 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -207,22 +207,30 @@ exports.postback = async (req, res) => { if (values.origin === "OneLink" && parsedComment.userEmail) { //Send an email, it was a text to pay link. - const endPoints = getEndpoints(); - sendTaskEmail({ - to: parsedComment.userEmail, - subject: `New Payment(s) Received - RO ${jobs.jobs.map((j) => j.ro_number).join(", ")}`, - type: "html", - html: generateEmailTemplate({ - header: "New Payment(s) Received", - subHeader: "", - body: jobs.jobs - .map( - (job) => - `Reference: ${job.ro_number || "N/A"} | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()} | $${partialPayments.find((p) => p.jobid === job.id).amount}` - ) - .join("
") - }) - }); + try { + const endPoints = getEndpoints(); + sendTaskEmail({ + to: parsedComment.userEmail, + subject: `New Payment(s) Received - RO ${jobs.jobs.map((j) => j.ro_number).join(", ")}`, + type: "html", + html: generateEmailTemplate({ + header: "New Payment(s) Received", + subHeader: "", + body: jobs.jobs + .map( + (job) => + `Reference: ${job.ro_number || "N/A"} | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()} | $${partialPayments.find((p) => p.jobid === job.id).amount}` + ) + .join("
") + }) + }); + } catch (error) { + logger.log("intellipay-postback-app-email-error", "DEBUG", req.user?.email, JSON.stringify(jobs), { + iprequest: values, + paymentResult, + error: error.message + }); + } } res.sendStatus(200); } else if (values.invoice) { From f287ba2dac8d5d2401badab2d4a40ce247739c9a Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 24 Sep 2024 10:35:46 -0400 Subject: [PATCH 036/145] IO-2931-Visual-Production-Board-Drag-and-Drop-on-Touch-Devices - Remove reference to /public on apple-touch-icon Signed-off-by: Dave Richer --- client/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/index.html b/client/index.html index 2ba9559dc..0d1e9bcfe 100644 --- a/client/index.html +++ b/client/index.html @@ -17,7 +17,7 @@ - +