Compare commits

...

47 Commits

Author SHA1 Message Date
Dave Richer
f53fcc345e hotfix/IO-3148-Error-In-Email-Bounce-Route: Hot Fix for Broken Import 2025-02-20 14:01:11 -05:00
Dave Richer
67e904e121 Merged in release/2025-02-28 (pull request #2120)
feature/IO-3146-Hotfix-For-Email-Translations
2025-02-19 15:41:14 +00:00
Dave Richer
83ea51157d Merged in feature/IO-3146-Hotfix-For-Email-Translation (pull request #2118)
feature/IO-3146-Hotfix-For-Email-Translations
2025-02-19 15:40:05 +00:00
Dave Richer
9f207f0946 feature/IO-3146-Hotfix-For-Email-Translations 2025-02-19 10:38:32 -05:00
Dave Richer
8a88a241d6 Merged in release/2025-02-14 (pull request #2117)
Release/2025-02-14 - IO-3127 IO-3128 IO-3077 IO-3131 IO-3139 IO-3140
2025-02-16 01:20:59 +00:00
Allan Carr
4684bada1e Merged in feature/IO-3140-Job-Close-Print-Center (pull request #2115)
IO-3140 Job Close Print Center

Approved-by: Dave Richer
2025-02-13 01:05:17 +00:00
Allan Carr
163354f4b4 IO-3140 Job Close Print Center
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-02-12 16:19:17 -08:00
Dave Richer
f3b2edea1c Merged in feature/IO-3139-Header-Deprecation-Bug (pull request #2114)
feature/IO-3139-Header-Deprecation-Bug - Quick fix for header deprecation.
2025-02-12 20:21:12 +00:00
Dave Richer
01e103fd0e feature/IO-3139-Header-Deprecation-Bug - Quick fix for header deprecation. 2025-02-12 15:20:17 -05:00
Dave Richer
4b184d1d42 Merged in hotfix/IO-3128-Unread-Messages-Not-Updating (pull request #2112)
hotfix/IO-3128-Unread-Messages-Not-Updating - Initial fix just to make sure clients see messages, will poll and update every 60 seconds if the chat window is closed and has never been opened.
2025-02-12 19:06:13 +00:00
Patrick Fic
8c541dad05 Merge branch 'release/2025-02-14' of bitbucket.org:snaptsoft/bodyshop into release/2025-02-14 2025-02-12 07:42:20 -08:00
Patrick Fic
921cca86c1 Remove patrick from support emails. 2025-02-12 07:41:52 -08:00
Allan Carr
841312ebcd Merged in feature/IO-3131-Crisp-Segment-for-BASIC (pull request #2111)
IO-3131 Crisp Segment for Basic

Approved-by: Dave Richer
2025-02-12 14:55:08 +00:00
Allan Carr
5ed00eaffe IO-3131 Crisp Segment for Basic
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-02-11 20:26:44 -08:00
Patrick Fic
024b4fe21b Merged in feature/IO-3077-import-rules-engine (pull request #2110)
Feature/IO-3077 import rules engine
2025-02-11 18:52:08 +00:00
Patrick Fic
40aca91c76 IO-3077 Move parts scan order to correctly update totals. 2025-02-11 10:51:26 -08:00
Dave Richer
abe4f4fb3d feature/IO-3077-import-rules-engine - Fix Translations 2025-02-11 12:34:28 -05:00
Patrick Fic
35a3726cf0 IO-3077 Implement import rules engine. 2025-02-10 14:24:50 -08:00
Patrick Fic
95a592fb9a Merged in feature/IO-3127-Dashboard-Schedule-Translations (pull request #2106)
IO-3127 Dashboard Schedule Translations

Approved-by: Dave Richer
2025-02-10 14:44:02 +00:00
Dave Richer
760f2ac7f9 hotfix/IO-3128-Unread-Messages-Not-Updating - Initial fix just to make sure clients see messages, will poll and update every 60 seconds if the chat window is closed and has never been opened. 2025-02-06 12:49:51 -05:00
Allan Carr
872e36a61a Merged in hotfix/2025-02-06 (pull request #2108)
IO-3121 Adjust Footer
2025-02-06 16:33:52 +00:00
Allan Carr
779f608506 Merged in feature/IO-3121-Generic-Report-Header (pull request #2107)
IO-3121 Adjust Footer
2025-02-06 16:32:50 +00:00
Allan Carr
d9f562faa4 IO-3121 Adjust Footer
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-02-06 08:32:52 -08:00
Patrick Fic
14e362ec3f Merged in release/2025-01-31 (pull request #2105)
Release/2025 01 31 - IO-1582, IO-2676, IO-2681, IO-2825, IO-2952, IO-2970, IO-3074, IO-3075, IO-3076, IO-3096, IO-3101, IO-3103, IO-3114, IO-3115, IO-3116, IO-3121, IO-3123
2025-02-06 04:01:53 +00:00
Patrick Fic
c213e13624 Resolve CI issues. 2025-02-05 20:00:48 -08:00
Patrick Fic
dae7642a8c Merged in release/2025-01-31 (pull request #2104)
Release/2025 01 31 - IO-1582, IO-2676, IO-2681, IO-2825, IO-2952, IO-2970, IO-3074, IO-3075, IO-3076, IO-3096, IO-3101, IO-3103, IO-3114, IO-3115, IO-3116, IO-3121, IO-3123
2025-02-06 03:56:56 +00:00
Allan Carr
c751f0cba4 IO-3127 Dashboard Schedule Translations
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-02-05 15:10:18 -08:00
Allan Carr
e128c108f8 Merged in feature/IO-3121-Generic-Report-Header (pull request #2102)
IO-3121 Generic Report Header

Approved-by: Dave Richer
2025-02-05 15:18:45 +00:00
Allan Carr
b8b76cb96c IO-3121 Generic Report Header
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-02-04 18:11:48 -08:00
Allan Carr
fa958cbbfe IO-3121 Generic Report Header
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-02-04 17:42:40 -08:00
Dave Richer
2146672916 release/2025-01-31 - fix teams icon 2025-02-04 13:47:58 -05:00
Dave Richer
f96460f332 Merged in feature/IO-2825-Node-22-Update (pull request #2092)
[DO NOT MERGE] Feature/IO-2825 Node 22 Update

Approved-by: Patrick Fic
2025-02-04 17:54:06 +00:00
Dave Richer
04c70876d0 Merged release/2025-01-31 into feature/IO-2825-Node-22-Update 2025-02-04 17:53:52 +00:00
Dave Richer
bc6a94eede Merged in feature/IO-3123-Code-Review-Adjustments (pull request #2099)
feature/IO-3123-Code-Review-Adjustments: Make Code review adjustments
2025-02-04 17:53:36 +00:00
Dave Richer
f288b0ee22 feature/IO-3123-Code-Review-Adjustments: Make Code review adjustments 2025-02-04 12:28:22 -05:00
Patrick Fic
e54692928b Merged in feature/IO-3076-report-trigger (pull request #2098)
IO-3076 Add cron trigger for RO usage report.

Approved-by: Patrick Fic
2025-02-04 16:28:55 +00:00
Dave Richer
07b18836f5 feature/IO-2825-Node-22-Update: Fix Header styling 2025-02-03 16:11:14 -05:00
Dave Richer
ff08d19d79 Merged release/2025-01-31 into feature/IO-2825-Node-22-Update 2025-02-03 16:46:01 +00:00
Dave Richer
68584243f4 Merge remote-tracking branch 'origin/release/2025-01-31' into feature/IO-2825-Node-22-Update 2025-01-30 15:37:37 -05:00
Dave Richer
994d7e17aa feature/IO-2825-Node-22-Update - Package Updates 2025-01-30 15:36:05 -05:00
Dave Richer
fd1dd6dddd Merge remote-tracking branch 'origin/release/2025-01-31' into feature/IO-2825-Node-22-Update 2025-01-30 15:32:31 -05:00
Dave Richer
1e9b82ba1e feature/IO-2825-Node-22-Update - Merge release 2025-01-30 15:32:11 -05:00
Dave Richer
da41668b3f feature/IO-3096-Notification-Preferences - Update Circle CI to Node 22 2025-01-22 10:56:53 -08:00
Dave Richer
df008abec9 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-3096-Global-Notification-Preferences 2025-01-22 10:53:42 -08:00
Dave Richer
29c99f2dd9 feature/IO-3096-Global-Notification-Preferences - Upgrade Node to 22 / Remove canvas and replace it 100% with canvas-skia 2025-01-20 09:02:55 -08:00
Dave Richer
3033e84f45 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-3096-Global-Notification-Preferences 2025-01-20 08:33:31 -08:00
Dave Richer
18966476e4 feature/IO-3096-Global-Notifications-Preferences
-  Package Updates
2025-01-17 09:26:30 -08:00
35 changed files with 19347 additions and 17059 deletions

View File

@@ -9,13 +9,13 @@ orbs:
jobs:
imex-api-deploy:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
steps:
- checkout
- eb/setup
- run:
command: |
eb init imex-online-production-api -r ca-central-1 -p "Node.js 18 running on 64bit Amazon Linux 2"
eb init imex-online-production-api -r ca-central-1 -p "Node.js 22 running on 64bit Amazon Linux 2023"
eb status --verbose
eb deploy
eb status
@@ -28,7 +28,7 @@ jobs:
imex-hasura-migrate:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
parameters:
secret:
type: string
@@ -52,7 +52,7 @@ jobs:
pipeline_number: << pipeline.number >>
imex-app-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
resource_class: large
working_directory: ~/repo/client
steps:
@@ -77,7 +77,7 @@ jobs:
imex-app-beta-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
resource_class: large
working_directory: ~/repo/client
@@ -114,7 +114,7 @@ jobs:
- eb/setup
- run:
command: |
eb init romeonline-productionapi -r us-east-2 -p "Node.js 18 running on 64bit Amazon Linux 2"
eb init romeonline-productionapi -r us-east-2 -p "Node.js 22 running on 64bit Amazon Linux 2023"
eb status --verbose
eb deploy
eb status
@@ -126,7 +126,7 @@ jobs:
pipeline_number: << pipeline.number >>
rome-hasura-migrate:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
parameters:
secret:
type: string
@@ -150,7 +150,7 @@ jobs:
pipeline_number: << pipeline.number >>
rome-app-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
working_directory: ~/repo/client
@@ -181,7 +181,7 @@ jobs:
test-rome-hasura-migrate:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
parameters:
secret:
type: string
@@ -208,7 +208,7 @@ jobs:
test-rome-app-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
working_directory: ~/repo/client
@@ -239,7 +239,7 @@ jobs:
test-hasura-migrate:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
parameters:
secret:
type: string
@@ -266,7 +266,7 @@ jobs:
imex-test-app-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
resource_class: large
working_directory: ~/repo/client
@@ -286,7 +286,7 @@ jobs:
imex-test-app-beta-build:
docker:
- image: cimg/node:18.18.2
- image: cimg/node:22.13.1
resource_class: large
working_directory: ~/repo/client

View File

@@ -3,7 +3,7 @@ FROM amazonlinux:2023
# Install Git and Node.js (Amazon Linux 2023 uses the DNF package manager)
RUN dnf install -y git \
&& curl -sL https://rpm.nodesource.com/setup_20.x | bash - \
&& curl -sL https://rpm.nodesource.com/setup_22.x | bash - \
&& dnf install -y nodejs \
&& dnf clean all

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1">
<babeledit_project be_version="2.7.1" version="1.2">
<!--
BabelEdit project file
@@ -6453,6 +6453,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>mark_critical</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>operation</name>
<definition_loaded>false</definition_loaded>
@@ -6474,6 +6495,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>update_field</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>update_value</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>value</name>
<definition_loaded>false</definition_loaded>
@@ -11943,6 +12006,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>shop_enabled_features</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>shopinfo</name>
<definition_loaded>false</definition_loaded>
@@ -12312,6 +12396,37 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>tooltips</name>
<children>
<folder_node>
<name>md_parts_scan</name>
<children>
<concept_node>
<name>update_value_tooltip</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
</folder_node>
<folder_node>
<name>validation</name>
<children>
@@ -19091,6 +19206,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ok</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>previous</name>
<definition_loaded>false</definition_loaded>
@@ -19385,6 +19521,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>sharetoteams</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>submit</name>
<definition_loaded>false</definition_loaded>
@@ -43090,6 +43247,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>parts_returns</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>print</name>
<definition_loaded>false</definition_loaded>
@@ -48557,6 +48735,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>unassigned</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>vertical</name>
<definition_loaded>false</definition_loaded>
@@ -52732,6 +52931,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>purchases_by_date_excel</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>purchases_by_date_range_detail</name>
<definition_loaded>false</definition_loaded>
@@ -54483,6 +54703,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>view</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>

9684
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,74 +8,74 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.8",
"@ant-design/pro-layout": "^7.22.0",
"@apollo/client": "^3.12.6",
"@emotion/is-prop-valid": "^1.3.1",
"@fingerprintjs/fingerprintjs": "^4.5.0",
"@fingerprintjs/fingerprintjs": "^4.5.1",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.36.2",
"@reduxjs/toolkit": "^2.5.0",
"@sentry/cli": "^2.40.0",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.13.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.1",
"@tanem/react-nprogress": "^5.0.53",
"@vitejs/plugin-react": "^4.3.4",
"antd": "^5.23.1",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
"axios": "^1.7.7",
"axios": "^1.7.9",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.13",
"dayjs-business-days2": "^1.2.2",
"dayjs-business-days2": "^1.2.3",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"dotenv": "^16.4.7",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.13.2",
"graphql": "^16.9.0",
"graphql": "^16.10.0",
"i18next": "^23.15.1",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-browser-languagedetector": "^8.0.2",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.9",
"libphonenumber-js": "^1.11.18",
"logrocket": "^8.1.2",
"markerjs2": "^2.32.2",
"markerjs2": "^2.32.3",
"memoize-one": "^6.0.0",
"normalize-url": "^8.0.1",
"object-hash": "^3.0.0",
"prop-types": "^15.8.1",
"query-string": "^9.1.0",
"query-string": "^9.1.1",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.14.1",
"react-big-calendar": "^1.17.1",
"react-color": "^2.19.3",
"react-cookie": "^7.2.0",
"react-cookie": "^7.2.2",
"react-dom": "^18.3.1",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.1.3",
"react-icons": "^5.3.0",
"react-icons": "^5.4.0",
"react-image-lightbox": "^5.1.4",
"react-markdown": "^9.0.1",
"react-number-format": "^5.4.2",
"react-markdown": "^9.0.3",
"react-number-format": "^5.4.3",
"react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.61",
"react-redux": "^9.1.2",
"react-redux": "^9.2.0",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.26.2",
"react-sticky": "^6.0.3",
"react-virtuoso": "^4.10.4",
"recharts": "^2.12.7",
"recharts": "^2.15.0",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.79.3",
"socket.io-client": "^4.8.0",
"styled-components": "^6.1.13",
"sass": "^1.83.4",
"socket.io-client": "^4.8.1",
"styled-components": "^6.1.14",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"userpilot": "^1.3.6",
@@ -120,36 +120,36 @@
"@rollup/rollup-linux-x64-gnu": "4.6.1"
},
"devDependencies": {
"@ant-design/icons": "^5.5.1",
"@ant-design/icons": "^5.5.2",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.24.7",
"@dotenvx/dotenvx": "^1.14.1",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/react": "^11.13.3",
"@eslint/js": "^9.15.0",
"@babel/preset-react": "^7.26.3",
"@dotenvx/dotenvx": "^1.33.0",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@eslint/js": "^9.18.0",
"@sentry/webpack-plugin": "^2.22.4",
"@testing-library/cypress": "^10.0.2",
"browserslist": "^4.23.3",
"browserslist": "^4.24.4",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.3.0",
"chalk": "^5.4.1",
"cross-env": "^7.0.3",
"cypress": "^13.14.2",
"cypress": "^13.17.0",
"eslint": "^8.57.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.12.0",
"memfs": "^4.12.0",
"eslint-plugin-react": "^7.37.4",
"globals": "^15.14.0",
"memfs": "^4.17.0",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.4.7",
"vite-plugin-babel": "^1.2.0",
"vite": "^6.0.7",
"vite-plugin-babel": "^1.3.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.5",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-style-import": "^2.0.0",
"workbox-window": "^7.1.0"
"workbox-window": "^7.3.0"
}
}

View File

@@ -5,6 +5,13 @@
border-bottom: 1px solid #74695c !important;
}
// TODO: This was added because the newest release of ant was making the text color and the background color the same on a selected header
// Tried all available tokens (https://ant.design/components/menu?locale=en-US) and even reverted all our custom styles, to no avail
// This should be kept an eye on, especially if implementing DARK MODE
.ant-menu-submenu-title {
color: rgba(255, 255, 255, 0.65) !important;
}
.imex-table-header {
display: flex;
flex-wrap: wrap;

View File

@@ -42,8 +42,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: chatVisible, // Skip when chat is visible
...(pollInterval > 0 ? { pollInterval } : {})
pollInterval: 60 * 1000 // TODO: This is a fix for now, should be coming from sockets
});
// Socket connection status
@@ -85,29 +84,25 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
// Get unread count from the cache
const unreadCount = (() => {
if (chatVisible) {
try {
const cachedData = client.readQuery({
query: CONVERSATION_LIST_QUERY,
variables: { offset: 0 }
});
try {
const cachedData = client.readQuery({
query: CONVERSATION_LIST_QUERY,
variables: { offset: 0 }
});
if (!cachedData?.conversations) return 0;
// Aggregate unread message count
return cachedData.conversations.reduce((total, conversation) => {
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
return total + unread;
}, 0);
} catch (error) {
console.warn("Unread count not found in cache:", error);
return 0; // Fallback if not in cache
if (!cachedData?.conversations) {
return unreadData?.messages_aggregate?.aggregate?.count;
}
} else if (unreadData?.messages_aggregate?.aggregate?.count) {
// Use the unread count from the query result
return unreadData.messages_aggregate.aggregate.count;
// Aggregate unread message count
return cachedData.conversations.reduce((total, conversation) => {
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
return total + unread;
}, 0);
} catch (error) {
console.warn("Unread count not found in cache:", error);
return 0; // Fallback if not in cache
}
return 0;
})();
return (

View File

@@ -1,16 +1,15 @@
import i18next from "i18next";
import React from "react";
import { connect } from "react-redux";
import { setUserLanguage } from "../../redux/user/user.actions";
import HeaderComponent from "./header.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
const mapDispatchToProps = (dispatch) => ({
setUserLanguage: (language) => dispatch(setUserLanguage(language))
});
// const mapDispatchToProps = (dispatch) => ({
// setUserLanguage: (language) => dispatch(setUserLanguage(language))
// });
export function HeaderContainer({ setUserLanguage }) {
const handleMenuClick = (e) => {
// setUserLanguage was removed from signature because it is not used in the component, and it is throwing a deprecation warning
export function HeaderContainer() {
// Commented out the handleMenuClick function because it is not used in the component, and it is throwing a deprecation warning
/* const handleMenuClick = (e) => {
if (e.item.props.actiontype === "lang-select") {
i18next.changeLanguage(e.key, (err, t) => {
if (err) {
@@ -23,9 +22,10 @@ export function HeaderContainer({ setUserLanguage }) {
setUserLanguage(e.key);
});
}
};
};*/
// return <HeaderComponent handleMenuClick={handleMenuClick} />;
return <HeaderComponent handleMenuClick={handleMenuClick} />;
return <HeaderComponent />;
}
export default connect(null, mapDispatchToProps)(HeaderContainer);
export default connect(null, null)(HeaderContainer);

View File

@@ -62,6 +62,9 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
refetchQueries: ["GET_LINE_TICKET_BY_PK"]
});
if (!r.errors) {
if (CriticalPartsScanning.treatment === "on") {
await CriticalPartsScan(jobLineEditModal.context.jobid, notification);
}
await Axios.post("/job/totalsssu", {
id: jobLineEditModal.context.jobid
});
@@ -107,7 +110,9 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
})
});
}
if (CriticalPartsScanning.treatment === "on") {
await CriticalPartsScan(jobLineEditModal.context.jobid, notification);
}
if (jobLineEditModal.actions.submit) {
jobLineEditModal.actions.submit();
} else {
@@ -115,9 +120,7 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
}
toggleModalVisible();
}
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(jobLineEditModal.context.jobid, notification);
}
setLoading(false);
};

View File

@@ -172,13 +172,13 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
job: newJob
}
});
if (CriticalPartsScanning.treatment === "on") {
await CriticalPartsScan(r.data.insert_jobs.returning[0].id, notification);
}
await Axios.post("/job/totalsssu", {
id: r.data.insert_jobs.returning[0].id
});
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(r.data.insert_jobs.returning[0].id, notification);
}
notification["success"]({
message: t("jobs.successes.created"),
onClick: () => {
@@ -281,6 +281,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id, notification);
}
if (updateResult.errors) {
//error while inserting
notification["error"]({

View File

@@ -134,10 +134,10 @@ export function JobsDetailHeaderActions({
const notification = useNotification();
const {
treatments: { ImEXPay, Share_To_Teams }
treatments: { ImEXPay }
} = useSplitTreatments({
attributes: {},
names: ["ImEXPay", "Share_To_Teams"],
names: ["ImEXPay"],
splitKey: bodyshop && bodyshop.imexshopid
});
@@ -202,7 +202,10 @@ export function JobsDetailHeaderActions({
message: t("appointments.successes.created")
});
} catch (error) {
notification.open({ type: "error", message: t("appointments.errors.saving", { error: error.message }) });
notification.open({
type: "error",
message: t("appointments.errors.saving", { error: error.message })
});
} finally {
setLoading(false);
setVisibility(false);
@@ -839,7 +842,7 @@ export function JobsDetailHeaderActions({
id: "job-actions-addtoproduction",
disabled: !job.converted,
label: t("jobs.actions.addtoproduction"),
onClick: () => AddToProduction(client, job.id, refetch, notification)
onClick: () => AddToProduction(client, job.id, refetch, false, notification)
}
);
@@ -972,7 +975,7 @@ export function JobsDetailHeaderActions({
}
);
if (Share_To_Teams?.treatment === "on") {
if (bodyshop?.md_functionality_toggles?.teams) {
menuItems.push({
key: "sharetoteams",
id: "job-actions-sharetoteams",

View File

@@ -13,6 +13,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser,
@@ -39,7 +40,7 @@ export function JobsDetailHeaderActionsToggleProduction({
const [form] = Form.useForm();
const notification = useNotification();
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
const [getJobDetails, { loading: jobDetailsLoading }] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
variables: { id: job.id },
onCompleted: (data) => {
if (data?.jobs_by_pk) {
@@ -109,65 +110,69 @@ export function JobsDetailHeaderActionsToggleProduction({
const popMenu = (
<div onClick={(e) => e.stopPropagation()}>
<Form layout="vertical" form={form} onFinish={handleConvert}>
{scenario === "pre" && (
<>
<Form.Item
name={["actual_in"]}
label={t("jobs.fields.actual_in")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item
name={["scheduled_completion"]}
label={t("jobs.fields.scheduled_completion")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
</>
)}
{scenario === "prod" && (
<>
<Form.Item
name={["actual_completion"]}
label={t("jobs.fields.actual_completion")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
{jobDetailsLoading ? (
<LoadingSpinner />
) : (
<Form layout="vertical" form={form} onFinish={handleConvert}>
{scenario === "pre" && (
<>
<Form.Item
name={["actual_in"]}
label={t("jobs.fields.actual_in")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item
name={["scheduled_completion"]}
label={t("jobs.fields.scheduled_completion")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
</>
)}
{scenario === "prod" && (
<>
<Form.Item
name={["actual_completion"]}
label={t("jobs.fields.actual_completion")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
</>
)}
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
</>
)}
<Space wrap>
<Button type="primary" onClick={() => form.submit()} loading={loading}>
{t("general.actions.save")}
</Button>
</Space>
</Form>
<Space wrap>
<Button type="primary" onClick={() => form.submit()} loading={loading}>
{t("general.actions.save")}
</Button>
</Space>
</Form>
)}
</div>
);

View File

@@ -67,7 +67,6 @@ export function PartsOrderListTableComponent({
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
// label: <ShareToTeamsButton noIcon={true} urlOverride={`${window.location.origin}${window.location.pathname}`} />
const recordActions = (record, showView = false) => (
<Space direction="horizontal" wrap>
<ShareToTeamsButton

View File

@@ -1,5 +1,4 @@
import { Button, Input, Space, Spin } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -24,19 +23,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilte
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
const { t } = useTranslation();
const [alertFilter, setAlertFilter] = useState(false);
const [unassignedFilter, setUnassignedFilter] = useState(false);
const toggleAlertFilter = () => {
const newAlertFilter = !alertFilter;
setAlertFilter(newAlertFilter);
setFilter({ ...filter, alert: newAlertFilter });
setFilter({ ...filter, alert: !filter.alert });
};
const toggleUnassignedFilter = () => {
const newUnassignedFilter = !unassignedFilter;
setUnassignedFilter(newUnassignedFilter);
setFilter({ ...filter, unassigned: newUnassignedFilter });
setFilter({ ...filter, unassigned: !filter.unassigned });
};
return (
@@ -58,16 +51,16 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
allowClear
/>
<Button
type={alertFilter ? "primary" : "default"}
type={filter?.alert ? "primary" : "default"}
onClick={toggleAlertFilter}
icon={alertFilter ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
icon={filter?.alert ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
>
{t("production.labels.alerts")}
</Button>
<Button
type={unassignedFilter ? "primary" : "default"}
type={filter?.unassigned ? "primary" : "default"}
onClick={toggleUnassignedFilter}
icon={unassignedFilter ? <UserDeleteOutlined /> : <UsergroupDeleteOutlined />}
icon={filter?.unassigned ? <UserDeleteOutlined /> : <UsergroupDeleteOutlined />}
>
{t("production.labels.unassigned")}
</Button>

View File

@@ -21,7 +21,7 @@ import dayjs from "../../utils/day";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
import { SiMicrosoftteams } from "react-icons/si";
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
@@ -424,7 +424,7 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
noIcon={true}
linkText={
<div className="share-to-teams-badge">
<SiMicrosoftteams />
<PiMicrosoftTeamsLogo />
</div>
}
urlOverride={`${window.location.origin}/manage/jobs/${card.id}`}

View File

@@ -1,9 +1,8 @@
import PropTypes from "prop-types";
import { Button } from "antd";
import { useLocation } from "react-router-dom";
import { SiMicrosoftteams } from "react-icons/si";
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
import { useTranslation } from "react-i18next";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
@@ -43,14 +42,6 @@ const ShareToTeamsComponent = ({
const location = useLocation();
const { t } = useTranslation();
const {
treatments: { Share_To_Teams }
} = useSplitTreatments({
attributes: {},
names: ["Share_To_Teams"],
splitKey: bodyshop && bodyshop.imexshopid
});
const currentUrl =
urlOverride ||
encodeURIComponent(`${window.location.origin}${location.pathname}${location.search}${location.hash}`);
@@ -78,7 +69,8 @@ const ShareToTeamsComponent = ({
window.open(teamsShareUrl, "_blank", windowFeatures);
};
if (Share_To_Teams?.treatment !== "on") {
// Feature is disabled
if (!bodyshop?.md_functionality_toggles?.teams) {
return null;
}
@@ -98,7 +90,7 @@ const ShareToTeamsComponent = ({
color: "#FFFFFF",
...buttonStyle
}}
icon={<SiMicrosoftteams style={{ color: "#FFFFFF", ...buttonIconStyle }} />}
icon={<PiMicrosoftTeamsLogo style={{ color: "#FFFFFF", ...buttonIconStyle }} />}
onClick={handleShare}
title={linkText === null ? t("general.actions.sharetoteams") : linkText}
/>

View File

@@ -14,7 +14,7 @@ import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
// TODO: Client Update, this might break
const timeZonesList = Intl.supportedValuesOf("timeZone");
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -642,6 +642,15 @@ export function ShopInfoGeneral({ form, bodyshop }) {
<Input />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.shop_enabled_features")} id="sharing">
<Form.Item
label={t("general.actions.sharetoteams")}
valuePropName="checked"
name={["md_functionality_toggles", "teams"]}
>
<Switch />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")} id="messagingpresets">
<Form.List name={["md_messaging_presets"]}>
{(fields, { add, remove, move }) => {

View File

@@ -1,16 +1,27 @@
import {DeleteFilled} from "@ant-design/icons";
import {Button, Col, Form, Input, Row, Select, Space, Switch} from "antd";
import React, {useMemo} from "react";
import {useTranslation} from "react-i18next";
import { DeleteFilled } from "@ant-design/icons";
import { Button, Col, Form, Input, Row, Select, Space, Switch } from "antd";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import i18n from "i18next";
const predefinedPartTypes = [
"PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"
];
const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"];
const predefinedModLbrTypes = [
"LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU",
"LA1", "LA2", "LA3", "LA4"
"LAA",
"LAB",
"LAD",
"LAE",
"LAF",
"LAG",
"LAM",
"LAR",
"LAS",
"LAU",
"LA1",
"LA2",
"LA3",
"LA4"
];
const getFieldType = (field) => {
@@ -20,30 +31,46 @@ const getFieldType = (field) => {
return null;
};
export default function ShopInfoPartsScan({form}) {
const {t} = useTranslation();
const fieldSelectOptions = [
{ label: i18n.t("joblines.fields.line_desc"), value: "line_desc" },
{ label: i18n.t("joblines.fields.part_type"), value: "part_type" },
{ label: i18n.t("joblines.fields.act_price"), value: "act_price" },
{ label: i18n.t("joblines.fields.part_qty"), value: "part_qty" },
{ label: i18n.t("joblines.fields.mod_lbr_ty"), value: "mod_lbr_ty" },
{
label: `${i18n.t("joblines.fields.oem_partno")} / ${i18n.t("joblines.fields.alt_partno")}`,
value: "part_number"
},
{ label: i18n.t("joblines.fields.op_code_desc"), value: "op_code_desc" }
];
export default function ShopInfoPartsScan({ form }) {
const { t } = useTranslation();
const watchedFields = Form.useWatch("md_parts_scan", form);
const operationOptions = useMemo(() => ({
string: [
{label: t("bodyshop.operations.contains"), value: "contains"},
{label: t("bodyshop.operations.equals"), value: "equals"},
{label: t("bodyshop.operations.starts_with"), value: "startsWith"},
{label: t("bodyshop.operations.ends_with"), value: "endsWith"},
],
number: [
{label: t("bodyshop.operations.equals"), value: "="},
{label: t("bodyshop.operations.greater_than"), value: ">"},
{label: t("bodyshop.operations.less_than"), value: "<"},
],
}), [t]);
const operationOptions = useMemo(
() => ({
string: [
{ label: t("bodyshop.operations.contains"), value: "contains" },
{ label: t("bodyshop.operations.equals"), value: "equals" },
{ label: t("bodyshop.operations.starts_with"), value: "startsWith" },
{ label: t("bodyshop.operations.ends_with"), value: "endsWith" }
],
number: [
{ label: t("bodyshop.operations.equals"), value: "=" },
{ label: t("bodyshop.operations.greater_than"), value: ">" },
{ label: t("bodyshop.operations.less_than"), value: "<" }
]
}),
[t]
);
return (
<div>
<LayoutFormRow header={t("bodyshop.labels.md_parts_scan")}>
<Form.List name={["md_parts_scan"]}>
{(fields, {add, remove, move}) => (
{(fields, { add, remove, move }) => (
<div>
{fields.map((field, index) => {
const selectedField = watchedFields?.[index]?.field || "line_desc";
@@ -61,28 +88,17 @@ export default function ShopInfoPartsScan({form}) {
{
required: true,
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.field"),
}),
},
label: t("bodyshop.fields.md_parts_scan.field")
})
}
]}
>
<Select
options={[
{label: t("joblines.fields.line_desc"), value: "line_desc"},
{label: t("joblines.fields.part_type"), value: "part_type"},
{label: t("joblines.fields.act_price"), value: "act_price"},
{label: t("joblines.fields.part_qty"), value: "part_qty"},
{label: t("joblines.fields.mod_lbr_ty"), value: "mod_lbr_ty"},
{label: t("joblines.fields.mod_lb_hrs"), value: "mod_lb_hrs"},
{
label: `${t("joblines.fields.oem_partno")} / ${t("joblines.fields.alt_partno")}`,
value: "part_number"
},
]}
options={fieldSelectOptions}
onChange={() => {
form.setFields([
{name: ["md_parts_scan", index, "operation"], value: "contains"},
{name: ["md_parts_scan", index, "value"], value: undefined},
{ name: ["md_parts_scan", index, "operation"], value: "contains" },
{ name: ["md_parts_scan", index, "value"], value: undefined }
]);
}}
/>
@@ -99,12 +115,12 @@ export default function ShopInfoPartsScan({form}) {
{
required: true,
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.operation"),
}),
},
label: t("bodyshop.fields.md_parts_scan.operation")
})
}
]}
>
<Select options={operationOptions[fieldType]}/>
<Select options={operationOptions[fieldType]} />
</Form.Item>
</Col>
)}
@@ -119,9 +135,9 @@ export default function ShopInfoPartsScan({form}) {
{
required: true,
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.value"),
}),
},
label: t("bodyshop.fields.md_parts_scan.value")
})
}
]}
>
{fieldType === "predefined" ? (
@@ -129,17 +145,17 @@ export default function ShopInfoPartsScan({form}) {
options={
selectedField === "part_type"
? predefinedPartTypes.map((type) => ({
label: type,
value: type
}))
label: type,
value: type
}))
: predefinedModLbrTypes.map((type) => ({
label: type,
value: type
}))
label: type,
value: type
}))
}
/>
) : (
<Input/>
<Input />
)}
</Form.Item>
</Col>
@@ -152,19 +168,70 @@ export default function ShopInfoPartsScan({form}) {
label={t("bodyshop.fields.md_parts_scan.caseInsensitive")}
name={[field.name, "caseInsensitive"]}
valuePropName="checked"
labelCol={{span: 14}}
wrapperCol={{span: 10}}
initialValue={true}
labelCol={{ span: 14 }}
wrapperCol={{ span: 10 }}
>
<Switch defaultChecked={true}/>
<Switch />
</Form.Item>
</Col>
)}
{/* Mark Line as Critical */}
<Col span={4}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.mark_critical")}
name={[field.name, "mark_critical"]}
valuePropName="checked"
initialValue={true}
labelCol={{ span: 14 }}
wrapperCol={{ span: 10 }}
>
<Switch />
</Form.Item>
</Col>
{/* Update Field */}
<Col span={4}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.update_field")}
name={[field.name, "update_field"]}
>
<Select
options={fieldSelectOptions}
allowClear
onClear={() =>
form.setFields([{ name: ["md_parts_scan", index, "update_field"], value: null }])
}
/>
</Form.Item>
</Col>
{/* Update Field */}
<Col span={4}>
<Form.Item
label={t("bodyshop.fields.md_parts_scan.update_value")}
name={[field.name, "update_value"]}
dependencies={[["md_parts_scan", index, "update_field"]]}
tooltip={t("bodyshop.tooltips.md_parts_scan.update_value_tooltip")}
rules={[
{
required: form.getFieldValue(["md_parts_scan", index, "update_field"]),
message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.update_value")
})
}
]}
>
<Input />
</Form.Item>
</Col>
{/* Actions */}
<Col span={2}>
<Space>
<DeleteFilled onClick={() => remove(field.name)}/>
<FormListMoveArrows move={move} index={index} total={fields.length}/>
<DeleteFilled onClick={() => remove(field.name)} />
<FormListMoveArrows move={move} index={index} total={fields.length} />
</Space>
</Col>
</Row>
@@ -175,8 +242,8 @@ export default function ShopInfoPartsScan({form}) {
<Form.Item>
<Button
type="dashed"
onClick={() => add({field: "line_desc", operation: "contains"})}
style={{width: "100%"}}
onClick={() => add({ field: "line_desc", operation: "contains" })}
style={{ width: "100%" }}
>
{t("bodyshop.actions.addpartsrule")}
</Button>

View File

@@ -269,7 +269,7 @@ function TaskListComponent({
<Space direction="horizontal">
<ShareToTeamsButton
linkText=""
urlOverride={`https://localhost:3000/manage/tasks/alltasks?taskid=${record.id}`}
urlOverride={`${window.location.origin}/manage/tasks/alltasks?taskid=${record.id}`}
/>
<Button
title={t("tasks.buttons.edit")}

View File

@@ -1890,6 +1890,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
kmout
qb_multiple_payers
lbr_adjustments
ownr_ea
payments {
amount
created_at

View File

@@ -235,7 +235,12 @@ export function* signInSuccessSaga({ payload }) {
try {
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]);
window.$crisp.push(["set", "session:segments", [["user"]]]);
const currentUserSegment = InstanceRenderManager({
imex: "imex-online-user",
rome: "rome-online-user"
});
window.$crisp.push(["set", "session:segments", [[currentUserSegment]]]);
InstanceRenderManager({
executeFunction: true,
args: [],
@@ -342,6 +347,9 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
}
});
payload.features?.allAccess === true
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
: window.$crisp.push(["set", "session:segments", [["basic"]]]);
} catch (error) {
console.error("Couldnt find $crisp.");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@ import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios";
import { TemplateList } from "./TemplateConstants";
import { generateTemplate } from "./graphQLmodifier";
import InstanceRenderManager from "./instanceRenderMgr";
const server = import.meta.env.VITE_APP_REPORTS_SERVER_URL;
@@ -39,7 +38,7 @@ export default async function RenderTemplate(
jsreport.headers["Authorization"] = jsrAuth;
//Query assets that match the template name. Must be in format <<templateName>>.query
let { contextData, useShopSpecificTemplate } = await fetchContextData(templateObject, jsrAuth);
let { contextData, useShopSpecificTemplate, shopSpecificFolder } = await fetchContextData(templateObject, jsrAuth);
const { ignoreCustomMargins } = Templates[templateObject.name];
@@ -74,14 +73,8 @@ export default async function RenderTemplate(
...contextData,
...templateObject.variables,
...templateObject.context,
headerpath: `/${InstanceRenderManager({
imex: bodyshop.imexshopid,
rome: bodyshop.imexshopid
})}/header.html`,
footerpath: `/${InstanceRenderManager({
imex: bodyshop.imexshopid,
rome: bodyshop.imexshopid
})}/footer.html`,
headerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
footerpath: shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
bodyshop: bodyshop,
filters: templateObject?.filters,
sorters: templateObject?.sorters,
@@ -149,11 +142,12 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
templateObjects.forEach((template) => {
proms.push(
(async () => {
let { contextData, useShopSpecificTemplate } = await fetchContextData(template, jsrAuth);
let { contextData, useShopSpecificTemplate, shopSpecificFolder } = await fetchContextData(template, jsrAuth);
unsortedTemplatesAndData.push({
templateObject: template,
contextData,
useShopSpecificTemplate
useShopSpecificTemplate,
shopSpecificFolder
});
})()
);
@@ -248,8 +242,8 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
// ...rootTemplate.templateObject.variables,
// ...rootTemplate.templateObject.context,
headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`,
headerpath: rootTemplate.shopSpecificFolder ? `/${bodyshop.imexshopid}/header.html` : `/GENERIC/header.html`,
footerpath: rootTemplate.shopSpecificFolder ? `/${bodyshop.imexshopid}/footer.html` : `/GENERIC/footer.html`,
bodyshop: bodyshop,
offset: bodyshop.timezone
}
@@ -397,10 +391,10 @@ const fetchContextData = async (templateObject, jsrAuth) => {
});
contextData = data;
}
return { contextData, useShopSpecificTemplate };
return { contextData, useShopSpecificTemplate, shopSpecificFolder };
}
return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate);
return await generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder);
};
//export const displayTemplateInWindow = (html) => {

View File

@@ -1,6 +1,6 @@
import { gql } from "@apollo/client";
import { Kind, parse, print, visit } from "graphql";
import client from "./GraphQLClient";
import { gql } from "@apollo/client";
/* eslint-disable no-loop-func */
@@ -114,9 +114,10 @@ export function printQuery(query) {
* @param templateQueryToExecute
* @param templateObject
* @param useShopSpecificTemplate
* @returns {Promise<{contextData: {}, useShopSpecificTemplate}>}
* @param shopSpecificTemplate
* @returns {Promise<{contextData: {}, useShopSpecificTemplate, shopSpecificTemplate}>}
*/
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) {
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate, shopSpecificFolder) {
// Advanced Filtering and Sorting modifications start here
// Parse the query and apply the filters and sorters
@@ -147,7 +148,7 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
contextData = data;
}
return { contextData, useShopSpecificTemplate };
return { contextData, useShopSpecificTemplate, shopSpecificFolder };
}
/**

View File

@@ -6,8 +6,8 @@ import eslint from "vite-plugin-eslint";
import { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
import chalk from "chalk";
//import { visualizer } from "rollup-plugin-visualizer";
// Ensure your environment variables are set correctly for Vite 6
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
timeZone: "America/Los_Angeles"
});
@@ -22,7 +22,7 @@ export const logger = createLogger("info", {
export default defineConfig({
base: "/",
plugins: [
//visualizer(),
// Ensure all plugins are Vite 6 compatible
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
VitePWA({
injectRegister: "auto",
@@ -31,14 +31,12 @@ export default defineConfig({
short_name: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "ImEX Online",
rome: "Rome Online",
rome: "Rome Online"
}),
name: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "ImEX Online",
rome: "Rome Online",
rome: "Rome Online"
}),
description: "The ultimate bodyshop management system.",
icons: [
@@ -46,7 +44,7 @@ export default defineConfig({
src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "favicon.png",
rome: "ro-favicon.png",
rome: "ro-favicon.png"
}),
sizes: "64x64 32x32 24x24 16x16",
type: "image/x-icon"
@@ -55,7 +53,7 @@ export default defineConfig({
src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "logo192.png",
rome: "logo192.png",
rome: "logo192.png"
}),
type: "image/png",
sizes: "192x192"
@@ -64,7 +62,7 @@ export default defineConfig({
src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "logo512.png",
rome: "ro-favicon.png",
rome: "ro-favicon.png"
}),
type: "image/png",
sizes: "512x512"
@@ -73,7 +71,7 @@ export default defineConfig({
theme_color: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "#1890ff",
rome: "#fff",
rome: "#fff"
}),
background_color: "#fff",
gcm_sender_id: "103953800507"
@@ -204,8 +202,10 @@ export default defineConfig({
"react-redux"
],
esbuildOptions: {
// Update for Vite 6: Use proper file extensions
loader: {
".js": "jsx"
".jsx": "jsx",
".tsx": "tsx"
}
}
},

3052
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,12 +19,12 @@
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
},
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.693.0",
"@aws-sdk/client-elasticache": "^3.693.0",
"@aws-sdk/client-s3": "^3.693.0",
"@aws-sdk/client-secrets-manager": "^3.693.0",
"@aws-sdk/client-ses": "^3.693.0",
"@aws-sdk/credential-provider-node": "^3.693.0",
"@aws-sdk/client-cloudwatch-logs": "^3.738.0",
"@aws-sdk/client-elasticache": "^3.738.0",
"@aws-sdk/client-s3": "^3.738.0",
"@aws-sdk/client-secrets-manager": "^3.738.0",
"@aws-sdk/client-ses": "^3.738.0",
"@aws-sdk/credential-provider-node": "^3.738.0",
"@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0",
@@ -33,7 +33,6 @@
"better-queue": "^3.8.12",
"bluebird": "^3.7.2",
"body-parser": "^1.20.3",
"canvas": "^2.11.2",
"chart.js": "^4.4.6",
"cloudinary": "^2.5.1",
"compression": "^1.7.5",
@@ -41,31 +40,31 @@
"cors": "2.8.5",
"crisp-status-reporter": "^1.2.2",
"csrf": "^3.1.0",
"dd-trace": "^5.28.0",
"dd-trace": "^5.33.1",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"firebase-admin": "^13.0.0",
"graphql": "^16.9.0",
"firebase-admin": "^13.0.2",
"graphql": "^16.10.0",
"graphql-request": "^6.1.0",
"inline-css": "^4.0.2",
"inline-css": "^4.0.3",
"intuit-oauth": "^4.1.3",
"ioredis": "^5.4.1",
"json-2-csv": "^5.5.6",
"ioredis": "^5.4.2",
"json-2-csv": "^5.5.8",
"juice": "^11.0.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.46",
"moment-timezone": "^0.5.47",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.6",
"node-persist": "^4.0.3",
"nodemailer": "^6.9.16",
"phone": "^3.1.53",
"node-persist": "^4.0.4",
"nodemailer": "^6.10.0",
"phone": "^3.1.58",
"recursive-diff": "^1.0.9",
"redis": "^4.7.0",
"rimraf": "^6.0.1",
"skia-canvas": "^2.0.0",
"soap": "^1.1.6",
"skia-canvas": "^2.0.2",
"soap": "^1.1.7",
"socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^11.0.0",
@@ -77,12 +76,12 @@
"xmlbuilder2": "^3.1.1"
},
"devDependencies": {
"@eslint/js": "^9.15.0",
"@eslint/js": "^9.19.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2",
"eslint": "^9.15.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.12.0",
"eslint": "^9.19.0",
"eslint-plugin-react": "^7.37.4",
"globals": "^15.14.0",
"p-limit": "^3.1.0",
"prettier": "^3.3.3",
"source-map-explorer": "^2.5.2"

View File

@@ -55,7 +55,7 @@ exports.default = async (req, res) => {
const csv = converter.json2csv(shopList, { emptyFieldValue: "" });
emailer
.sendTaskEmail({
to: ["patrick.fic@convenient-brands.com", "bradley.rhoades@convenient-brands.com"],
to: ["patrick.fic@convenient-brands.com", "bradley.rhoades@convenient-brands.com", "jrome@rometech.com"],
subject: `RO Usage Report - ${moment().format("MM/DD/YYYY")}`,
text: `
Usage Report for ${moment().format("MM/DD/YYYY")} for Rome Online Customers.

View File

@@ -55,7 +55,7 @@ const sendServerEmail = async ({ subject, text }) => {
imex: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
rome: `Rome Online API - ${process.env.NODE_ENV} <noreply@romeonline.io>`
}),
to: ["patrick@imexsystems.ca", "support@thinkimex.com"],
to: ["support@thinkimex.com"],
subject: subject,
text: text,
ses: {
@@ -92,7 +92,7 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
},
(err, info) => {
// (message, type, user, record, meta
logger.log("server-email", err ? "error" : "debug", null, null, { message: err || info });
logger.log("server-email", err ? "error" : "debug", null, null, { message: err ? err?.message : info });
}
);
} catch (error) {
@@ -239,24 +239,24 @@ const emailBounce = async (req, res) => {
return;
}
//If it's bounced, log it as bounced in audit log. Send an email to the user.
const result = await client.request(queries.UPDATE_EMAIL_AUDIT, {
await client.request(queries.UPDATE_EMAIL_AUDIT, {
sesid: messageId,
status: "Bounced",
context: message.bounce?.bouncedRecipients
});
mailer.sendMail(
{
from: InstanceMgr({
from: InstanceManager({
imex: `ImEX Online <noreply@imex.online>`,
rome: `Rome Online <noreply@romeonline.io>`
}),
to: replyTo,
//bcc: "patrick@snapt.ca",
subject: `${InstanceMgr({
subject: `${InstanceManager({
imex: "ImEX Online",
rome: "Rome Online"
})} Bounced Email - RE: ${subject}`,
text: `${InstanceMgr({
text: `${InstanceManager({
imex: "ImEX Online",
rome: "Rome Online"
})} has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error.
@@ -270,7 +270,7 @@ ${body.bounce?.bouncedRecipients.map(
},
(err, info) => {
logger.log("sns-error", err ? "error" : "debug", "api", null, {
message: err ? JSON.stringify(error) : info
message: err ? err?.message : info
});
}
);

View File

@@ -2241,6 +2241,7 @@ exports.QUERY_PARTS_SCAN = `query QUERY_PARTS_SCAN ($id: uuid!) {
mod_lb_hrs
oem_partno
alt_partno
op_code_desc
}
}
}`;
@@ -2252,7 +2253,7 @@ exports.UPDATE_PARTS_CRITICAL = `mutation UPDATE_PARTS_CRITICAL ($IdsToMarkCriti
notcritical: update_joblines(where: {id: {_nin: $IdsToMarkCritical}, jobid: {_eq: $jobid}}, _set: {critical: false}) {
affected_rows
}
}`
}`;
exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
associations(where: {active: {_eq: true}, useremail: {_eq: $user}}) {
@@ -2618,7 +2619,6 @@ exports.CREATE_CONVERSATION = `mutation CREATE_CONVERSATION($conversation: [conv
}
`;
exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: timestamptz!) {
bodyshops(where: { created_at: { _gte: $period } }) {
shopname
@@ -2689,4 +2689,19 @@ exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: time
}
}
}
`
`;
exports.INSERT_AUDIT_TRAIL = `
mutation INSERT_AUDIT_TRAIL($auditObj: audit_trail_insert_input!) {
insert_audit_trail_one(object: $auditObj) {
id
jobid
billid
bodyshopid
created
operation
type
useremail
}
}
`;

View File

@@ -1,16 +1,25 @@
const queries = require("../graphql-client/queries");
const logger = require("../utils/logger");
const predefinedPartTypes = [
"PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG",
];
const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"];
const predefinedModLbrTypes = [
"LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU",
"LA1", "LA2", "LA3", "LA4",
"LAA",
"LAB",
"LAD",
"LAE",
"LAF",
"LAG",
"LAM",
"LAR",
"LAS",
"LAU",
"LA1",
"LA2",
"LA3",
"LA4"
];
exports.partsScan = async function (req, res) {
console.log('hello')
const { jobid } = req.body;
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
@@ -18,10 +27,9 @@ exports.partsScan = async function (req, res) {
logger.log("job-parts-scan", "DEBUG", req.user?.email, jobid, null);
try {
const data = await client.setHeaders({ Authorization: BearerToken }).request(
queries.QUERY_PARTS_SCAN,
{ id: jobid }
);
const data = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_PARTS_SCAN, { id: jobid });
const rules = data.jobs_by_pk.bodyshop.md_parts_scan || [];
if (!Array.isArray(rules)) {
@@ -36,21 +44,22 @@ exports.partsScan = async function (req, res) {
? new RegExp(rule.value, "i")
: typeof rule.value === "string"
? new RegExp(rule.value)
: null,
: null
}));
const criticalIds = new Set();
for (const jobline of data.jobs_by_pk.joblines) {
for (const { field, regex, operation, value } of compiledRules) {
if (criticalIds.has(jobline.id)) break; // Skip further evaluation if already critical
for (const { field, regex, operation, value, mark_critical, update_field, update_value } of compiledRules) {
// IO-3077 - Remove skip as this is extended to include line updates.
// if (criticalIds.has(jobline.id)) break; // Skip further evaluation if already critical
let jobValue = jobline[field];
let match = false;
if (field === "part_number") {
match = regex
? regex.test(jobline.oem_partno || '') || regex.test(jobline.alt_partno || '')
? regex.test(jobline.oem_partno || "") || regex.test(jobline.alt_partno || "")
: jobline.oem_partno === value || jobline.alt_partno === value;
} else if (field === "part_type") {
match = predefinedPartTypes.includes(value) && value === jobValue;
@@ -68,22 +77,44 @@ exports.partsScan = async function (req, res) {
}
if (match) {
criticalIds.add(jobline.id);
break; // No need to evaluate further rules for this jobline
if (mark_critical) {
//Could technically lead to duplicates, but they'd only be n positives, ultimately leading a positive.
criticalIds.add(jobline.id);
}
if (update_field && update_value) {
const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.UPDATE_JOB_LINE, {
lineId: jobline.id,
line: { [update_field]: update_value, manual_line: true }
});
const auditResult = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_AUDIT_TRAIL, {
auditObj: {
bodyshopid: data.jobs_by_pk.bodyshop.id,
jobid,
operation: `Jobline (#${jobline.line_no} ${jobline.line_desc}/${jobline.id}) ${update_field} updated from ${jobline[update_field]} to ${update_value}. Lined marked as manual line.`,
type: "partscanupdate",
useremail: req.user.email
}
});
}
//break; // No need to evaluate further rules for this jobline
}
}
}
const result = await client.setHeaders({ Authorization: BearerToken }).request(
queries.UPDATE_PARTS_CRITICAL,
{ IdsToMarkCritical: Array.from(criticalIds), jobid }
);
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.UPDATE_PARTS_CRITICAL, { IdsToMarkCritical: Array.from(criticalIds), jobid });
res.status(200).json(result);
} catch (error) {
logger.log("job-parts-scan-error", "ERROR", req.user?.email, jobid, {
jobid,
error: error.message,
stack: error.stack
});
res.status(400).json(JSON.stringify({ message: error?.message }));
}

View File

@@ -1,4 +1,3 @@
const { createCanvas } = require("canvas");
const { Canvas, FontLibrary } = require("skia-canvas");
const Chart = require("chart.js/auto");
@@ -65,7 +64,7 @@ const getChartConfiguration = (keys, values, override) => {
return defaultsDeep(override || {}, defaultConfiguration);
};
const processCanvasRequest = async (req, res, isSkia = false) => {
const processCanvasRequest = async (req, res) => {
const { logger } = req;
const { w, h, values, keys, override } = req.body;
@@ -77,7 +76,6 @@ const processCanvasRequest = async (req, res, isSkia = false) => {
const configuration = getChartConfiguration(keys, values, override);
// Placeholders to allow fine control over GAC
let canvas = null;
let ctx = null;
let chart = null;
@@ -85,16 +83,15 @@ const processCanvasRequest = async (req, res, isSkia = false) => {
try {
// Create the canvas
canvas = isSkia ? new Canvas(width, height) : createCanvas(width, height);
canvas = new Canvas(width, height);
ctx = canvas.getContext("2d");
// Render the chart
chart = new Chart(ctx, configuration);
// Generate and send the image
chartImage = isSkia ? (await canvas.toBuffer("image/png")).toString("base64") : canvas.toDataURL();
res.status(200).send(isSkia ? `data:image/png;base64,${chartImage}` : chartImage);
chartImage = (await canvas.toBuffer("image/png")).toString("base64");
res.status(200).send(`data:image/png;base64,${chartImage}`);
} catch (error) {
// Log the error and send the response
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
@@ -104,27 +101,27 @@ const processCanvasRequest = async (req, res, isSkia = false) => {
if (chart) {
chart.destroy();
}
ctx = null; // Explicitly nullify for garbage collection
canvas = null; // Explicitly nullify for garbage collection
ctx = null;
canvas = null;
chartImage = null;
}
};
const enqueueRequest = (req, res, isSkia) => {
const enqueueRequest = (req, res) => {
if (requestQueue.length >= CANVAS_QUEUE_LIMIT) {
res.status(503).send("Server is busy. Please try again later.");
return false;
}
requestQueue.push({ req, res, isSkia });
requestQueue.push({ req, res });
req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
return true;
};
const processNextInQueue = async () => {
while (requestQueue.length > 0) {
const { req, res, isSkia } = requestQueue.shift();
const { req, res } = requestQueue.shift();
try {
await processCanvasRequest(req, res, isSkia);
await processCanvasRequest(req, res);
} catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
}
@@ -137,13 +134,7 @@ exports.canvastest = function (req, res) {
};
exports.canvas = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res, false)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
};
exports.canvasSkia = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res, true)) return;
if (isProcessing || !enqueueRequest(req, res)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
};

View File

@@ -2,12 +2,12 @@ const express = require("express");
const router = express.Router();
const { inlinecss } = require("../render/inlinecss");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { canvas, canvasSkia } = require("../render/canvas-handler");
const { canvas } = require("../render/canvas-handler");
const validateCanvasInputMiddleware = require("../middleware/validateCanvasInputMiddleware");
// Define the route for inline CSS rendering
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss);
router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvas);
router.post("/canvas-skia", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvasSkia);
router.post("/canvas-skia", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
router.post("/canvas", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
module.exports = router;