From 02974e6e4b0ffaaa131ce68af2b3019acd283741 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 11 Sep 2025 21:32:18 -0700 Subject: [PATCH 01/13] IO-3365 Push Filters to Query Signed-off-by: Allan Carr --- client/src/graphql/bills.queries.js | 4 +-- .../src/pages/bills/bills.page.component.jsx | 27 ++++++++++++++----- .../src/pages/bills/bills.page.container.jsx | 3 ++- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/client/src/graphql/bills.queries.js b/client/src/graphql/bills.queries.js index 7a7fd2d8a..a9c3433fa 100644 --- a/client/src/graphql/bills.queries.js +++ b/client/src/graphql/bills.queries.js @@ -20,8 +20,8 @@ export const DELETE_BILL = gql` `; export const QUERY_ALL_BILLS_PAGINATED = gql` - query QUERY_ALL_BILLS_PAGINATED($offset: Int, $limit: Int, $order: [bills_order_by!]!) { - bills(offset: $offset, limit: $limit, order_by: $order) { + query QUERY_ALL_BILLS_PAGINATED($offset: Int, $limit: Int, $order: [bills_order_by!]!, $where: bills_bool_exp) { + bills(offset: $offset, limit: $limit, order_by: $order, where: $where) { id vendorid vendor { diff --git a/client/src/pages/bills/bills.page.component.jsx b/client/src/pages/bills/bills.page.component.jsx index db9145984..4f06295af 100644 --- a/client/src/pages/bills/bills.page.component.jsx +++ b/client/src/pages/bills/bills.page.component.jsx @@ -31,7 +31,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte const history = useNavigate(); const [state, setState] = useLocalStorage("bills_list_sort", { sortedInfo: {}, - filteredInfo: { text: "" } + filteredInfo: { vendorname: [] } }); const Templates = TemplateList("bill"); const { t } = useTranslation(); @@ -48,8 +48,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte vendor: { name: order === "descend" ? "desc" : "asc" } }), filters: (vendorsData?.vendors || []).map((v) => ({ text: v.name, value: v.id })), - filteredValue: state.filteredInfo.vendorname || null, - onFilter: (value, record) => record.vendorid === value, + filteredValue: search.vendorIds ? search.vendorIds.split(",") : null, sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, render: (text, record) => {record.vendor.name} }, @@ -165,20 +164,36 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte ]; const handleTableChange = (pagination, filters, sorter) => { - // Persist filters (including vendorname) and sorting - setState({ ...state, filteredInfo: { ...state.filteredInfo, ...filters }, sortedInfo: sorter }); + setState({ + sortedInfo: sorter, + filteredInfo: { ...state.filteredInfo, vendorname: filters.vendorname || [] } + }); + search.page = pagination.current; + if (filters.vendorname && filters.vendorname.length) { + search.vendorIds = filters.vendorname.join(","); + } else { + delete search.vendorIds; + } if (sorter && sorter.column && sorter.column.sortObject) { search.searchObj = JSON.stringify(sorter.column.sortObject(sorter.order)); + delete search.sortcolumn; + delete search.sortorder; } else { delete search.searchObj; search.sortcolumn = sorter.order ? sorter.columnKey : null; search.sortorder = sorter.order; } - search.sort = JSON.stringify({ [sorter.columnKey]: sorter.order }); history({ search: queryString.stringify(search) }); }; + useEffect(() => { + if (!search.vendorIds && state.filteredInfo.vendorname && state.filteredInfo.vendorname.length) { + search.vendorIds = state.filteredInfo.vendorname.join(","); + history({ search: queryString.stringify(search) }); + } + }, []); + useEffect(() => { if (search.search && search.search.trim() !== "") { searchBills(); diff --git a/client/src/pages/bills/bills.page.container.jsx b/client/src/pages/bills/bills.page.container.jsx index 8ea150322..68c5d4c01 100644 --- a/client/src/pages/bills/bills.page.container.jsx +++ b/client/src/pages/bills/bills.page.container.jsx @@ -49,7 +49,8 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) { : { [sortcolumn || "date"]: sortorder ? (sortorder === "descend" ? "desc" : "asc") : "desc" } - ] + ], + where: searchParams.vendorIds ? { vendorid: { _in: searchParams.vendorIds.split(",") } } : undefined } }); From 2660466db1a989c55738dfe48b91bd5c9e60fb4b Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 15 Sep 2025 14:13:48 -0700 Subject: [PATCH 02/13] IO-3325 Add reverse proxy URL for amplitude. --- client/.env.development.imex | 3 ++- client/.env.development.rome | 3 ++- client/.env.production.imex | 3 ++- client/.env.production.rome | 3 ++- client/.env.test.imex | 3 ++- client/.env.test.rome | 3 ++- client/src/index.jsx | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/client/.env.development.imex b/client/.env.development.imex index 0b2eec83e..a7c9c1349 100644 --- a/client/.env.development.imex +++ b/client/.env.development.imex @@ -15,4 +15,5 @@ VITE_APP_INSTANCE=IMEX TEST_USERNAME="test@imex.dev" TEST_PASSWORD="test123" VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien -VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com \ No newline at end of file +VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file diff --git a/client/.env.development.rome b/client/.env.development.rome index fb4690ff6..816df9917 100644 --- a/client/.env.development.rome +++ b/client/.env.development.rome @@ -17,4 +17,5 @@ VITE_APP_INSTANCE=ROME TEST_USERNAME="test@imex.dev" TEST_PASSWORD="test123" VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien -VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com \ No newline at end of file +VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file diff --git a/client/.env.production.imex b/client/.env.production.imex index eaa7f6c56..dc1d7fe7a 100644 --- a/client/.env.production.imex +++ b/client/.env.production.imex @@ -14,4 +14,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.imex.online VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk VITE_APP_INSTANCE=IMEX VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien -VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com \ No newline at end of file +VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file diff --git a/client/.env.production.rome b/client/.env.production.rome index 64b33ade8..808c8e199 100644 --- a/client/.env.production.rome +++ b/client/.env.production.rome @@ -14,4 +14,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.romeonline.io VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk VITE_APP_INSTANCE=ROME VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien -VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com \ No newline at end of file +VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file diff --git a/client/.env.test.imex b/client/.env.test.imex index 5071568c5..2ff9a10d7 100644 --- a/client/.env.test.imex +++ b/client/.env.test.imex @@ -14,4 +14,5 @@ VITE_APP_IS_TEST=true VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_INSTANCE=IMEX VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien -VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com \ No newline at end of file +VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file diff --git a/client/.env.test.rome b/client/.env.test.rome index 708647c14..24c9b7047 100644 --- a/client/.env.test.rome +++ b/client/.env.test.rome @@ -14,4 +14,5 @@ VITE_APP_IS_TEST=true VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_INSTANCE=ROME VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien -VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com \ No newline at end of file +VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file diff --git a/client/src/index.jsx b/client/src/index.jsx index 830679c50..7132566a1 100644 --- a/client/src/index.jsx +++ b/client/src/index.jsx @@ -27,7 +27,8 @@ registerSW({ immediate: true }); Dinero.globalRoundingMode = "HALF_EVEN"; amplitude.init("6228a598e57cd66875cfd41604f1f891", { - defaultTracking: true + defaultTracking: true, + serverUrl: import.meta.env.VITE_APP_AMP_URL // { // attribution: { // excludeReferrers: true, From cc934fe333c3e21c8848b5e19acfa4c2847134db Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 16 Sep 2025 17:20:23 -0700 Subject: [PATCH 03/13] IO-3373 Dashboard Errors on Large Datasets Signed-off-by: Allan Carr --- .../dashboard-grid/createDashboardQuery.js | 10 +- .../dashboard-grid.component.jsx | 107 ++++++++++++------ 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/client/src/components/dashboard-grid/createDashboardQuery.js b/client/src/components/dashboard-grid/createDashboardQuery.js index 1b0ce0a3f..5f0e7b322 100644 --- a/client/src/components/dashboard-grid/createDashboardQuery.js +++ b/client/src/components/dashboard-grid/createDashboardQuery.js @@ -2,11 +2,13 @@ import { gql } from "@apollo/client"; import dayjs from "../../utils/day.js"; import componentList from "./componentList.js"; -const createDashboardQuery = (state) => { +const createDashboardQuery = (items) => { const componentBasedAdditions = - state && - Array.isArray(state.layout) && - state.layout.map((item) => componentList[item.i].gqlFragment || "").join(""); + Array.isArray(items) && + items + .map((item) => (componentList[item.i] && componentList[item.i].gqlFragment) || "") + .filter(Boolean) + .join(""); return gql` query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""} monthly_sales: jobs(where: {_and: [ diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx index 546481e2e..929681bbd 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx +++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx @@ -1,5 +1,5 @@ import Icon, { SyncOutlined } from "@ant-design/icons"; -import { cloneDeep, isEmpty } from "lodash"; +import { cloneDeep } from "lodash"; import { useMutation, useQuery } from "@apollo/client"; import { Button, Dropdown, Space } from "antd"; import { PageHeader } from "@ant-design/pro-layout"; @@ -34,14 +34,25 @@ const mapDispatchToProps = () => ({ export function DashboardGridComponent({ currentUser, bodyshop }) { const { t } = useTranslation(); - const [state, setState] = useState({ - ...(bodyshop.associations[0].user.dashboardlayout - ? bodyshop.associations[0].user.dashboardlayout - : { items: [], layout: {}, layouts: [] }) + const [state, setState] = useState(() => { + const persisted = bodyshop.associations[0].user.dashboardlayout; + // Normalize persisted structure to avoid malformed shapes that can cause recursive layout recalculations + if (persisted) { + return { + items: Array.isArray(persisted.items) ? persisted.items : [], + layout: Array.isArray(persisted.layout) ? persisted.layout : [], + layouts: typeof persisted.layouts === "object" && !Array.isArray(persisted.layouts) ? persisted.layouts : {}, + cols: persisted.cols + }; + } + return { items: [], layout: [], layouts: {}, cols: 12 }; }); const notification = useNotification(); - const { loading, error, data, refetch } = useQuery(createDashboardQuery(state), { + // Memoize the query document so Apollo doesn't treat each render as a brand-new query causing continuous re-fetches + const dashboardQueryDoc = useMemo(() => createDashboardQuery(state.items), [state.items]); + + const { loading, error, data, refetch } = useQuery(dashboardQueryDoc, { fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); @@ -49,21 +60,32 @@ export function DashboardGridComponent({ currentUser, bodyshop }) { const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT); const handleLayoutChange = async (layout, layouts) => { - logImEXEvent("dashboard_change_layout"); + try { + logImEXEvent("dashboard_change_layout"); - setState({ ...state, layout, layouts }); + setState((prev) => ({ ...prev, layout, layouts })); - const result = await updateLayout({ - variables: { - email: currentUser.email, - layout: { ...state, layout, layouts } + const result = await updateLayout({ + variables: { + email: currentUser.email, + layout: { ...state, layout, layouts } + } + }); + + if (result?.errors && result.errors.length) { + const errorMessages = result.errors.map((e) => e?.message || String(e)); + notification.error({ + message: t("dashboard.errors.updatinglayout", { + message: errorMessages.join("; ") + }) + }); } - }); - - if (!isEmpty(result?.errors)) { + } catch (err) { + // Catch any unexpected errors (including potential cyclic JSON issues) so the promise never rejects unhandled + console.error("Dashboard layout update failed", err); notification.error({ message: t("dashboard.errors.updatinglayout", { - message: JSON.stringify(result.errors) + message: err?.message || String(err) }) }); } @@ -80,19 +102,26 @@ export function DashboardGridComponent({ currentUser, bodyshop }) { }; const handleAddComponent = (e) => { - logImEXEvent("dashboard_add_component", { name: e }); - setState({ - ...state, - items: [ - ...state.items, + // Avoid passing the full AntD menu click event (contains circular refs) to analytics + logImEXEvent("dashboard_add_component", { key: e.key }); + const compSpec = componentList[e.key] || {}; + const minW = compSpec.minW || 1; + const minH = compSpec.minH || 1; + const baseW = compSpec.w || 2; + const baseH = compSpec.h || 2; + setState((prev) => { + const nextItems = [ + ...prev.items, { i: e.key, - x: (state.items.length * 2) % (state.cols || 12), - y: 99, // puts it at the bottom - w: componentList[e.key].w || 2, - h: componentList[e.key].h || 2 + // Position near bottom: use a large y so RGL places it last without triggering cascading relayout loops + x: (prev.items.length * 2) % (prev.cols || 12), + y: 1000, + w: Math.max(baseW, minW), + h: Math.max(baseH, minH) } - ] + ]; + return { ...prev, items: nextItems }; }); }; @@ -130,25 +159,33 @@ export function DashboardGridComponent({ currentUser, bodyshop }) { className="layout" breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} - width="100%" layouts={state.layouts} onLayoutChange={handleLayoutChange} > {state.items.map((item) => { - const TheComponent = componentList[item.i].component; + const spec = componentList[item.i] || {}; + const TheComponent = spec.component; + const minW = spec.minW || 1; + const minH = spec.minH || 1; + // Ensure current width/height respect minimums to avoid react-grid-layout prop warnings + const safeItem = { + ...item, + w: Math.max(item.w || spec.w || minW, minW), + h: Math.max(item.h || spec.h || minH, minH) + }; return (
handleRemoveComponent(item.i)} + onClick={() => handleRemoveComponent(safeItem.i)} /> - + {TheComponent && }
); From 4afff893c05d7bb5351e895f995cc1d840479090 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 17 Sep 2025 09:29:35 -0700 Subject: [PATCH 04/13] IO-3330 localEmailViewer Update Signed-off-by: Allan Carr --- _reference/localEmailViewer/index.js | 180 ++++++++---------- _reference/localEmailViewer/package-lock.json | 42 ++-- _reference/localEmailViewer/package.json | 2 +- 3 files changed, 102 insertions(+), 122 deletions(-) diff --git a/_reference/localEmailViewer/index.js b/_reference/localEmailViewer/index.js index 2b4c21444..1dd17f779 100644 --- a/_reference/localEmailViewer/index.js +++ b/_reference/localEmailViewer/index.js @@ -1,116 +1,96 @@ // index.js -import express from 'express'; -import fetch from 'node-fetch'; -import {simpleParser} from 'mailparser'; +import express from "express"; +import fetch from "node-fetch"; +import { simpleParser } from "mailparser"; const app = express(); const PORT = 3334; -app.get('/', async (req, res) => { - try { - const response = await fetch('http://localhost:4566/_aws/ses'); - if (!response.ok) { - throw new Error('Network response was not ok'); - } - const data = await response.json(); - const messagesHtml = await parseMessages(data.messages); - res.send(renderHtml(messagesHtml)); - } catch (error) { - console.error('Error fetching messages:', error); - res.status(500).send('Error fetching messages'); +app.get("/", async (req, res) => { + try { + const response = await fetch("http://localhost:4566/_aws/ses"); + if (!response.ok) { + throw new Error("Network response was not ok"); } + const data = await response.json(); + const messagesHtml = await parseMessages(data.messages); + res.send(renderHtml(messagesHtml)); + } catch (error) { + console.error("Error fetching messages:", error); + res.status(500).send("Error fetching messages"); + } }); async function parseMessages(messages) { - const parsedMessages = await Promise.all( - messages.map(async (message, index) => { - try { - const parsed = await simpleParser(message.RawData); - return ` -
-
-
- Message ${index + 1} -
-
- From: ${message.Source} -
-
- Region: ${message.Region} -
-
- Timestamp: ${message.Timestamp} -
-
-
- ${parsed.html || parsed.textAsHtml || 'No HTML content available'} -
-
- `; - } catch (error) { - console.error('Error parsing email:', error); - return ` -
-
- Message ${index + 1} -
-
- From: ${message.Source} -
-
- Region: ${message.Region} -
-
- Timestamp: ${message.Timestamp} -
-
- Error parsing email content -
-
- `; - } - }) - ); - return parsedMessages.join(''); + const parsedMessages = await Promise.all( + messages.map(async (message, index) => { + try { + const parsed = await simpleParser(message.RawData); + return ` +
+
+
Message ${index + 1}
+
From: ${message.Source}
+
To: ${parsed.to.text || "No To Address"}
+
Subject: ${parsed.subject || "No Subject"}
+
Region: ${message.Region}
+
Timestamp: ${message.Timestamp}
+
+
${parsed.html || parsed.textAsHtml || "No HTML content available"}
+
+ `; + } catch (error) { + console.error("Error parsing email:", error); + return ` +
+
Message ${index + 1}
+
From: ${message.Source}
+
Region: ${message.Region}
+
Timestamp: ${message.Timestamp}
+
Error parsing email content
+
+ `; + } + }) + ); + return parsedMessages.join(""); } function renderHtml(messagesHtml) { - return ` - - - - - - Email Messages Viewer - - - - -
-

Email Messages Viewer

-
- ${messagesHtml} -
-
- - - `; + return ` + + + + + + Email Messages Viewer + + + + +
+

Email Messages Viewer

+
${messagesHtml}
+
+ + + `; } app.listen(PORT, () => { - console.log(`Server is running on http://localhost:${PORT}`); -}); \ No newline at end of file + console.log(`Server is running on http://localhost:${PORT}`); +}); diff --git a/_reference/localEmailViewer/package-lock.json b/_reference/localEmailViewer/package-lock.json index ae14abb19..2c7ecad93 100644 --- a/_reference/localEmailViewer/package-lock.json +++ b/_reference/localEmailViewer/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "express": "^5.1.0", - "mailparser": "^3.7.2", + "mailparser": "^3.7.4", "node-fetch": "^3.3.2" } }, @@ -634,9 +634,9 @@ "license": "MIT" }, "node_modules/libmime": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.6.tgz", - "integrity": "sha512-j9mBC7eiqi6fgBPAGvKCXJKJSIASanYF4EeA4iBzSG0HxQxmXnR3KbyWqTn4CwsKSebqCv2f5XZfAO6sKzgvwA==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", "license": "MIT", "dependencies": { "encoding-japanese": "2.2.0", @@ -661,31 +661,31 @@ } }, "node_modules/mailparser": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.2.tgz", - "integrity": "sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.4.tgz", + "integrity": "sha512-Beh4yyR4jLq3CZZ32asajByrXnW8dLyKCAQD3WvtTiBnMtFWhxO+wa93F6sJNjDmfjxXs4NRNjw3XAGLqZR3Vg==", "license": "MIT", "dependencies": { "encoding-japanese": "2.2.0", "he": "1.2.0", "html-to-text": "9.0.5", "iconv-lite": "0.6.3", - "libmime": "5.3.6", + "libmime": "5.3.7", "linkify-it": "5.0.0", - "mailsplit": "5.4.2", - "nodemailer": "6.9.16", + "mailsplit": "5.4.5", + "nodemailer": "7.0.4", "punycode.js": "2.3.1", - "tlds": "1.255.0" + "tlds": "1.259.0" } }, "node_modules/mailsplit": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.2.tgz", - "integrity": "sha512-4cczG/3Iu3pyl8JgQ76dKkisurZTmxMrA4dj/e8d2jKYcFTZ7MxOzg1gTioTDMPuFXwTrVuN/gxhkrO7wLg7qA==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.5.tgz", + "integrity": "sha512-oMfhmvclR689IIaQmIcR5nODnZRRVwAKtqFT407TIvmhX2OLUBnshUTcxzQBt3+96sZVDud9NfSe1NxAkUNXEQ==", "license": "(MIT OR EUPL-1.1+)", "dependencies": { "libbase64": "1.3.0", - "libmime": "5.3.6", + "libmime": "5.3.7", "libqp": "2.1.1" } }, @@ -793,9 +793,9 @@ } }, "node_modules/nodemailer": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", - "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.4.tgz", + "integrity": "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -1114,9 +1114,9 @@ } }, "node_modules/tlds": { - "version": "1.255.0", - "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz", - "integrity": "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==", + "version": "1.259.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.259.0.tgz", + "integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==", "license": "MIT", "bin": { "tlds": "bin.js" diff --git a/_reference/localEmailViewer/package.json b/_reference/localEmailViewer/package.json index 74cef99ff..5f553cf17 100644 --- a/_reference/localEmailViewer/package.json +++ b/_reference/localEmailViewer/package.json @@ -12,7 +12,7 @@ "description": "", "dependencies": { "express": "^5.1.0", - "mailparser": "^3.7.2", + "mailparser": "^3.7.4", "node-fetch": "^3.3.2" } } From ac6856b136c4838156d7c07698399a4f12fd2dca Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 17 Sep 2025 13:59:20 -0700 Subject: [PATCH 05/13] IO-3373 Dashboard Component Infinite Recursion Signed-off-by: Allan Carr --- .../src/components/dashboard-grid/dashboard-grid.component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx index 546481e2e..f61a57398 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx +++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx @@ -80,7 +80,7 @@ export function DashboardGridComponent({ currentUser, bodyshop }) { }; const handleAddComponent = (e) => { - logImEXEvent("dashboard_add_component", { name: e }); + logImEXEvent("dashboard_add_component", { name: e.key }); setState({ ...state, items: [ From 038aa82087fe35585212b2d77c3ba6de0dfb6025 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 17 Sep 2025 14:43:07 -0700 Subject: [PATCH 06/13] IO-3330 CARFAX Datapump Adjustment Cron trigger and billing email Signed-off-by: Allan Carr --- docker-compose.yml | 1 + hasura/metadata/cron_triggers.yaml | 9 ++++++++ server/data/carfax.js | 36 +++++++++++++++++++++++++++++- server/email/sendemail.js | 36 ++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ae8b35048..ad805e272 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -119,6 +119,7 @@ services: aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1 aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1 aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1 + aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1 " # Node App: The Main IMEX API node-app: diff --git a/hasura/metadata/cron_triggers.yaml b/hasura/metadata/cron_triggers.yaml index 534560869..cc1baa225 100644 --- a/hasura/metadata/cron_triggers.yaml +++ b/hasura/metadata/cron_triggers.yaml @@ -6,6 +6,15 @@ headers: - name: x-imex-auth value_from_env: DATAPUMP_AUTH +- name: CARFAX Data Pump + webhook: '{{HASURA_API_URL}}/data/carfax' + schedule: 0 7 * * 6 + include_in_metadata: true + payload: {} + headers: + - name: x-imex-auth + value_from_env: DATAPUMP_AUTH + comment: Project Mexico - name: Chatter Data Pump webhook: '{{HASURA_API_URL}}/data/chatter' schedule: 45 5 * * * diff --git a/server/data/carfax.js b/server/data/carfax.js index b25e371a8..c1db22193 100644 --- a/server/data/carfax.js +++ b/server/data/carfax.js @@ -6,7 +6,7 @@ const InstanceManager = require("../utils/instanceMgr").default; const { isString, isEmpty } = require("lodash"); const fs = require("fs"); const client = require("../graphql-client/graphql-client").client; -const { sendServerEmail } = require("../email/sendemail"); +const { sendServerEmail, sendMexicoBillingEmail } = require("../email/sendemail"); const { uploadFileToS3 } = require("../utils/s3"); const crypto = require("crypto"); @@ -168,6 +168,29 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat await uploadViaSFTP(jsonObj); } + await sendMexicoBillingEmail({ + subject: `${shopid.toUpperCase()}_Mexico_${moment().format("MM-DD-YY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`, + text: `Errors:\n${JSON.stringify( + erroredJobs.map((ej) => ({ + ro_number: ej.job?.ro_number, + jobid: ej.job?.id, + error: ej.error + })), + null, + 2 + )}\n\nUploaded:\n${JSON.stringify( + { + bodyshopid: bodyshop.id, + imexshopid: shopid, + count: jsonObj.count, + filename: jsonObj.filename, + result: jsonObj.result + }, + null, + 2 + )}` + }); + allXMLResults.push({ bodyshopid: bodyshop.id, imexshopid: shopid, @@ -402,3 +425,14 @@ const generatePartType = (type) => { return partTypeMap[type?.toLowerCase()] || null; }; + +const errorCode = ({ count, filename, results }) => { + if (count === 0) return 1; + if (!filename) return 3; + const sftpErrorCode = results?.sftpError?.code; + if (sftpErrorCode && ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "ECONNRESET"].includes(sftpErrorCode)) { + return 4; + } + if (sftpErrorCode) return 7; + return 0; +}; diff --git a/server/email/sendemail.js b/server/email/sendemail.js index 6622cb6ce..c9a72c17f 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -79,6 +79,41 @@ const sendServerEmail = async ({ subject, text }) => { } }; +const sendMexicoBillingEmail = async ({ subject, text }) => { + if (process.env.NODE_ENV === undefined) return; + try { + mailer.sendMail( + { + from: InstanceManager({ + imex: `ImEX Online API - ${process.env.NODE_ENV} `, + rome: `Rome Online API - ${process.env.NODE_ENV} ` + }), + to: ["mexico@rometech.zohodesk.com"], + subject: subject, + text: text, + ses: { + // optional extra arguments for SendRawEmail + Tags: [ + { + Name: "tag_name", + Value: "tag_value" + } + ] + } + }, + // eslint-disable-next-line no-unused-vars + (err, info) => { + logger.log("server-email-failure", err ? "error" : "debug", null, null, { + message: err?.message, + stack: err?.stack + }); + } + ); + } catch (error) { + logger.log("server-email-failure", "error", null, null, { message: error?.message, stack: error?.stack }); + } +}; + const sendWelcomeEmail = async ({ to, resetLink, dateLine, features, bcc }) => { try { await mailer.sendMail({ @@ -420,6 +455,7 @@ ${body.bounce?.bouncedRecipients.map( module.exports = { sendEmail, sendServerEmail, + sendMexicoBillingEmail, sendTaskEmail, emailBounce, sendWelcomeEmail From 166a33af4ef27512e02204a80f9e6bcfe8dcc26a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 17 Sep 2025 16:07:15 -0700 Subject: [PATCH 07/13] IO-3376 Scrollbar Theming Signed-off-by: Allan Carr --- client/src/App/App.styles.scss | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index 616f88383..51dafcdfe 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -272,23 +272,23 @@ } // Scrollbar styles (uncomment if needed, updated for dark mode) -::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - border-radius: 0.2rem; - background-color: var(--table-stripe-bg); -} +// ::-webkit-scrollbar-track { +// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); +// border-radius: 0.2rem; +// background-color: var(--table-stripe-bg); +// } -::-webkit-scrollbar { - width: 0.25rem; - max-height: 0.25rem; - background-color: var(--table-stripe-bg); -} +// ::-webkit-scrollbar { +// width: 0.25rem; +// max-height: 0.25rem; +// background-color: var(--table-stripe-bg); +// } -::-webkit-scrollbar-thumb { - border-radius: 0.2rem; - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - background-color: var(--alert-color); -} +// ::-webkit-scrollbar-thumb { +// border-radius: 0.2rem; +// -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); +// background-color: var(--alert-color); +// } .ant-input-number-input, .ant-input-number, From f93800ded4ca6450583692a47d0f8b67d198738b Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 19 Sep 2025 09:51:38 -0700 Subject: [PATCH 08/13] IO-3325 Additional ImEX log Events. --- client/.env.development.imex | 3 ++- client/.env.development.rome | 3 ++- client/.env.production.imex | 3 ++- client/.env.production.rome | 3 ++- client/.env.test.imex | 3 ++- client/.env.test.rome | 3 ++- ...accounting-receivables-table.component.jsx | 11 +++++++++- .../bills-list-table.component.jsx | 4 ++++ .../card-payment-modal.component.jsx | 4 +++- .../contracts-find-modal.container.jsx | 1 - .../global-search-os.component.jsx | 3 +++ .../job-audit-trail.component.jsx | 2 ++ .../job-detail-lines/job-lines.component.jsx | 11 +++++++++- .../job-lifecycle/job-lifecycle.component.jsx | 20 ++++++++++--------- .../job-line-bulk-assign.component.jsx | 2 ++ .../job-line-dispatch-button.component.jsx | 2 ++ .../job-remove-from-parts-queue.component.jsx | 3 +++ .../jobs-available-scan.component.jsx | 5 +++++ .../jobs-available-table.component.jsx | 2 ++ ...etail-header-actions.toggle-production.jsx | 4 ++++ .../jobs-list-paginated.component.jsx | 2 ++ .../jobs-list/jobs-list.component.jsx | 2 ++ .../owner-detail-form.container.jsx | 13 +++++++----- ...rts-order-modal-price-change.component.jsx | 3 ++- .../parts-queue-card.component.jsx | 3 +++ .../parts-queue.list.component.jsx | 2 ++ .../production-board-filters.component.jsx | 9 ++++++++- ...duction-list-columns.comment.component.jsx | 2 ++ .../production-list-table.component.jsx | 4 ++++ .../qbo-authorize/qbo-authorize.component.jsx | 2 ++ ...hedule-calendar-header-graph.component.jsx | 8 +++++++- .../schedule-calendar-header.component.jsx | 3 +++ .../scheduler-calendar-wrapper.component.jsx | 3 +++ .../schedule-calendar.component.jsx | 3 +++ .../scoreboard-entry-edit.component.jsx | 2 ++ .../share-to-teams.component.jsx | 2 ++ .../simplified-parts-jobs-list.component.jsx | 4 ++-- .../task-list/task-list.component.jsx | 2 ++ .../task-list/task-list.container.jsx | 3 +++ .../task-upsert-modal.container.jsx | 5 +++-- .../vehicle-detail-form.container.jsx | 13 +++++++----- client/src/firebase/firebase.utils.js | 14 +++++++------ client/src/index.jsx | 2 +- .../src/pages/bills/bills.page.component.jsx | 4 ++++ .../contract-create.page.container.jsx | 2 ++ .../jobs-create/jobs-create.container.jsx | 4 +++- client/vite.config.js | 2 +- 47 files changed, 165 insertions(+), 45 deletions(-) diff --git a/client/.env.development.imex b/client/.env.development.imex index a7c9c1349..79d1b4e63 100644 --- a/client/.env.development.imex +++ b/client/.env.development.imex @@ -16,4 +16,5 @@ TEST_USERNAME="test@imex.dev" TEST_PASSWORD="test123" VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com -VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com +VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 \ No newline at end of file diff --git a/client/.env.development.rome b/client/.env.development.rome index 816df9917..eabf048e8 100644 --- a/client/.env.development.rome +++ b/client/.env.development.rome @@ -18,4 +18,5 @@ TEST_USERNAME="test@imex.dev" TEST_PASSWORD="test123" VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com -VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com +VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78 \ No newline at end of file diff --git a/client/.env.production.imex b/client/.env.production.imex index dc1d7fe7a..70c7c01c7 100644 --- a/client/.env.production.imex +++ b/client/.env.production.imex @@ -15,4 +15,5 @@ VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk VITE_APP_INSTANCE=IMEX VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com -VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com +VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 \ No newline at end of file diff --git a/client/.env.production.rome b/client/.env.production.rome index 808c8e199..cb2cd88ac 100644 --- a/client/.env.production.rome +++ b/client/.env.production.rome @@ -15,4 +15,5 @@ VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk VITE_APP_INSTANCE=ROME VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com -VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com +VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78 \ No newline at end of file diff --git a/client/.env.test.imex b/client/.env.test.imex index 2ff9a10d7..0afecd91b 100644 --- a/client/.env.test.imex +++ b/client/.env.test.imex @@ -15,4 +15,5 @@ VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_INSTANCE=IMEX VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com -VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com +VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 \ No newline at end of file diff --git a/client/.env.test.rome b/client/.env.test.rome index 24c9b7047..558c4528e 100644 --- a/client/.env.test.rome +++ b/client/.env.test.rome @@ -15,4 +15,5 @@ VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_INSTANCE=ROME VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com -VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com \ No newline at end of file +VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com +VITE_APP_AMP_KEY=46b1193a867d4e3131ae4c3a64a3fc78 \ No newline at end of file diff --git a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx index 7775fbb8f..a3aacdb08 100644 --- a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx +++ b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx @@ -142,7 +142,16 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r refetch={refetch} /> - + ) diff --git a/client/src/components/bills-list-table/bills-list-table.component.jsx b/client/src/components/bills-list-table/bills-list-table.component.jsx index 8b1d8e09c..166cd0b5b 100644 --- a/client/src/components/bills-list-table/bills-list-table.component.jsx +++ b/client/src/components/bills-list-table/bills-list-table.component.jsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { FaTasks } from "react-icons/fa"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; @@ -75,6 +76,7 @@ export function BillsListTableComponent({