Compare commits

..

66 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
Patrick Fic
0fd8bcb1b1 IO-3076 Add cron trigger for RO usage report. 2025-02-04 08:28:06 -08: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
bd6f300c8d Merged in feature/IO-2970-Production-Board-Unassigned-Filter (pull request #2096)
feature/IO-2970-Production-Board-Unassigned-Filter - Implementation
2025-01-31 18:24:50 +00:00
Dave Richer
ac2fbaf6f7 feature/IO-2970-Production-Board-Unassigned-Filter - Implementation 2025-01-31 13:23:36 -05:00
Allan Carr
f409acc7fd Merged in feature/IO-3116-Production-Flag-Translation (pull request #2093)
IO-3116 Production Flag Translation

Approved-by: Dave Richer
2025-01-31 15:59:15 +00:00
Allan Carr
06dcb20b2b Merged in feature/IO-3074-Mark-as-PST-Exempt-Job-Create (pull request #2094)
IO-3074 Mark as PST Exempt in Manaul Job Creation

Approved-by: Dave Richer
2025-01-31 15:58:42 +00:00
Allan Carr
f4fed0db9d IO-3074 Mark as PST Exempt in Manaul Job Creation
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-30 15:25:39 -08:00
Allan Carr
8430f500ef IO-3116 Production Flag Translation
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-30 14:30:47 -08: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
c8f5c3ed9e release/2025-01-31 - Fix unused import 2025-01-30 15:36:47 -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
312795618e Merged in feature/IO-2681-Share-To-Teams-Button (pull request #2090)
Feature/IO-2681 Share To Teams Button
2025-01-30 20:17:24 +00:00
Dave Richer
35b5645d6f feature/IO-2681-Share-To-Teams-Button - Missing translation 2025-01-30 15:16:56 -05:00
Dave Richer
a49d845f50 feature/IO-2681-Share-To-Teams-Button - Merge release 2025-01-30 15:13:48 -05:00
Dave Richer
9d44540ca8 feature/IO-2681-Share-To-Teams-Button - Final revisions. 2025-01-30 15:06:52 -05:00
Allan Carr
cc95d3bd44 Merged in feature/IO-3115-Print-Center-on-Job-Close (pull request #2088)
IO-3115 Print Center on Job Close

Approved-by: Dave Richer
2025-01-30 17:12:11 +00:00
Allan Carr
648c47cde2 IO-3115 Change Icon to internal button prop
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-30 09:12:58 -08:00
Allan Carr
17cf6e7696 Merged in feature/IO-1582-Temp-Docs-to-Exported-Files (pull request #2087)
IO-1582 Temp Docs to Exported/Invoiced Files

Approved-by: Dave Richer
2025-01-30 16:42:54 +00:00
Allan Carr
7326ffbae6 Merged in feature/IO-3114-Quick-Intake (pull request #2086)
Feature/IO-3114 Quick Intake

Approved-by: Dave Richer
2025-01-30 16:42:36 +00:00
Allan Carr
b2f73c4fba IO-3115 Print Center on Job Close
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-29 16:55:31 -08:00
Allan Carr
6628d43e12 IO-1582 Temp Docs to Exported/Invoiced Files
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2025-01-29 15:43:38 -08:00
Dave Richer
a064b8e07e feature/IO-2681-Share-To-Teams-Button - checkpoint 2025-01-29 18:19:34 -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
47 changed files with 19611 additions and 17083 deletions

View File

@@ -9,13 +9,13 @@ orbs:
jobs: jobs:
imex-api-deploy: imex-api-deploy:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
steps: steps:
- checkout - checkout
- eb/setup - eb/setup
- run: - run:
command: | 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 status --verbose
eb deploy eb deploy
eb status eb status
@@ -28,7 +28,7 @@ jobs:
imex-hasura-migrate: imex-hasura-migrate:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
parameters: parameters:
secret: secret:
type: string type: string
@@ -52,7 +52,7 @@ jobs:
pipeline_number: << pipeline.number >> pipeline_number: << pipeline.number >>
imex-app-build: imex-app-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
resource_class: large resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
steps: steps:
@@ -77,7 +77,7 @@ jobs:
imex-app-beta-build: imex-app-beta-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
resource_class: large resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
@@ -114,7 +114,7 @@ jobs:
- eb/setup - eb/setup
- run: - run:
command: | 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 status --verbose
eb deploy eb deploy
eb status eb status
@@ -126,7 +126,7 @@ jobs:
pipeline_number: << pipeline.number >> pipeline_number: << pipeline.number >>
rome-hasura-migrate: rome-hasura-migrate:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
parameters: parameters:
secret: secret:
type: string type: string
@@ -150,7 +150,7 @@ jobs:
pipeline_number: << pipeline.number >> pipeline_number: << pipeline.number >>
rome-app-build: rome-app-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
working_directory: ~/repo/client working_directory: ~/repo/client
@@ -181,7 +181,7 @@ jobs:
test-rome-hasura-migrate: test-rome-hasura-migrate:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
parameters: parameters:
secret: secret:
type: string type: string
@@ -208,7 +208,7 @@ jobs:
test-rome-app-build: test-rome-app-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
working_directory: ~/repo/client working_directory: ~/repo/client
@@ -239,7 +239,7 @@ jobs:
test-hasura-migrate: test-hasura-migrate:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
parameters: parameters:
secret: secret:
type: string type: string
@@ -266,7 +266,7 @@ jobs:
imex-test-app-build: imex-test-app-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
resource_class: large resource_class: large
working_directory: ~/repo/client working_directory: ~/repo/client
@@ -286,7 +286,7 @@ jobs:
imex-test-app-beta-build: imex-test-app-beta-build:
docker: docker:
- image: cimg/node:18.18.2 - image: cimg/node:22.13.1
resource_class: large resource_class: large
working_directory: ~/repo/client 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) # Install Git and Node.js (Amazon Linux 2023 uses the DNF package manager)
RUN dnf install -y git \ 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 install -y nodejs \
&& dnf clean all && 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 BabelEdit project file
@@ -6453,6 +6453,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>operation</name> <name>operation</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -6474,6 +6495,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>value</name> <name>value</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -11943,6 +12006,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>shopinfo</name> <name>shopinfo</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -12312,6 +12396,37 @@
</concept_node> </concept_node>
</children> </children>
</folder_node> </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> <folder_node>
<name>validation</name> <name>validation</name>
<children> <children>
@@ -19091,6 +19206,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>previous</name> <name>previous</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -19385,6 +19521,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>submit</name> <name>submit</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -43090,6 +43247,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>print</name> <name>print</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -48557,6 +48735,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>vertical</name> <name>vertical</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -52732,6 +52931,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>purchases_by_date_range_detail</name> <name>purchases_by_date_range_detail</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -54483,6 +54703,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>

9682
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,16 +1,15 @@
import i18next from "i18next";
import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { setUserLanguage } from "../../redux/user/user.actions";
import HeaderComponent from "./header.component"; import HeaderComponent from "./header.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
const mapDispatchToProps = (dispatch) => ({ // const mapDispatchToProps = (dispatch) => ({
setUserLanguage: (language) => dispatch(setUserLanguage(language)) // setUserLanguage: (language) => dispatch(setUserLanguage(language))
}); // });
export function HeaderContainer({ setUserLanguage }) { // setUserLanguage was removed from signature because it is not used in the component, and it is throwing a deprecation warning
const handleMenuClick = (e) => { 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") { if (e.item.props.actiontype === "lang-select") {
i18next.changeLanguage(e.key, (err, t) => { i18next.changeLanguage(e.key, (err, t) => {
if (err) { if (err) {
@@ -23,9 +22,10 @@ export function HeaderContainer({ setUserLanguage }) {
setUserLanguage(e.key); 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"] refetchQueries: ["GET_LINE_TICKET_BY_PK"]
}); });
if (!r.errors) { if (!r.errors) {
if (CriticalPartsScanning.treatment === "on") {
await CriticalPartsScan(jobLineEditModal.context.jobid, notification);
}
await Axios.post("/job/totalsssu", { await Axios.post("/job/totalsssu", {
id: jobLineEditModal.context.jobid 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) { if (jobLineEditModal.actions.submit) {
jobLineEditModal.actions.submit(); jobLineEditModal.actions.submit();
} else { } else {
@@ -115,9 +120,7 @@ function JobLinesUpsertModalContainer({ jobLineEditModal, toggleModalVisible, bo
} }
toggleModalVisible(); toggleModalVisible();
} }
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(jobLineEditModal.context.jobid, notification);
}
setLoading(false); setLoading(false);
}; };

View File

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

View File

@@ -1,26 +1,23 @@
import { Collapse, Form, Input, InputNumber, Select, Space, Switch } from "antd"; import { Collapse, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component"; import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
import JobsDetailChangeFilehandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component"; import JobsDetailChangeFilehandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component"; import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
import JobsDetailRatesLabor from "../jobs-detail-rates/jobs-detail-rates.labor.component"; import JobsDetailRatesLabor from "../jobs-detail-rates/jobs-detail-rates.labor.component";
import JobsDetailRatesMaterials from "../jobs-detail-rates/jobs-detail-rates.materials.component"; import JobsDetailRatesMaterials from "../jobs-detail-rates/jobs-detail-rates.materials.component";
import JobsDetailRatesOther from "../jobs-detail-rates/jobs-detail-rates.other.component"; import JobsDetailRatesOther from "../jobs-detail-rates/jobs-detail-rates.other.component";
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.component"; import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.component";
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component"; import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
@@ -199,7 +196,9 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Collapse.Panel> </Collapse.Panel>
<Collapse.Panel forceRender key="financial" header={t("menus.jobsdetail.financials")}> <Collapse.Panel forceRender key="financial" header={t("menus.jobsdetail.financials")}>
<JobsDetailRatesChangeButton form={form} /> <JobsDetailRatesChangeButton form={form} />
<JobsMarkPstExempt form={form} /> {InstanceRenderManager({
imex: <JobsMarkPstExempt form={form} />
})}
<LayoutFormRow> <LayoutFormRow>
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt"> <Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
<CurrencyInput min={0} /> <CurrencyInput min={0} />
@@ -246,7 +245,6 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</LayoutFormRow> </LayoutFormRow>
) )
})} })}
<LayoutFormRow> <LayoutFormRow>
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab"> <Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
<CurrencyInput /> <CurrencyInput />

View File

@@ -32,6 +32,7 @@ import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production"; import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -201,7 +202,10 @@ export function JobsDetailHeaderActions({
message: t("appointments.successes.created") message: t("appointments.successes.created")
}); });
} catch (error) { } 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 { } finally {
setLoading(false); setLoading(false);
setVisibility(false); setVisibility(false);
@@ -838,7 +842,7 @@ export function JobsDetailHeaderActions({
id: "job-actions-addtoproduction", id: "job-actions-addtoproduction",
disabled: !job.converted, disabled: !job.converted,
label: t("jobs.actions.addtoproduction"), label: t("jobs.actions.addtoproduction"),
onClick: () => AddToProduction(client, job.id, refetch, notification) onClick: () => AddToProduction(client, job.id, refetch, false, notification)
} }
); );
@@ -971,6 +975,14 @@ export function JobsDetailHeaderActions({
} }
); );
if (bodyshop?.md_functionality_toggles?.teams) {
menuItems.push({
key: "sharetoteams",
id: "job-actions-sharetoteams",
label: <ShareToTeamsButton noIcon={true} urlOverride={`${window.location.origin}${window.location.pathname}`} />
});
}
menuItems.push({ menuItems.push({
key: "exportcustdata", key: "exportcustdata",
id: "job-actions-exportcustdata", id: "job-actions-exportcustdata",

View File

@@ -13,6 +13,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { DateTimeFormatterFunction } from "../../utils/DateFormatter"; import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component"; import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser, //currentUser: selectCurrentUser,
@@ -39,7 +40,7 @@ export function JobsDetailHeaderActionsToggleProduction({
const [form] = Form.useForm(); const [form] = Form.useForm();
const notification = useNotification(); 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 }, variables: { id: job.id },
onCompleted: (data) => { onCompleted: (data) => {
if (data?.jobs_by_pk) { if (data?.jobs_by_pk) {
@@ -109,6 +110,9 @@ export function JobsDetailHeaderActionsToggleProduction({
const popMenu = ( const popMenu = (
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
{jobDetailsLoading ? (
<LoadingSpinner />
) : (
<Form layout="vertical" form={form} onFinish={handleConvert}> <Form layout="vertical" form={form} onFinish={handleConvert}>
{scenario === "pre" && ( {scenario === "pre" && (
<> <>
@@ -168,6 +172,7 @@ export function JobsDetailHeaderActionsToggleProduction({
</Button> </Button>
</Space> </Space>
</Form> </Form>
)}
</div> </div>
); );

View File

@@ -1,14 +1,14 @@
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import { Button, Form, Popover, Space } from "antd"; import { Button, Form, Popover, Space } from "antd";
import axios from "axios"; import axios from "axios";
import React, { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries"; import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import JobSearchSelect from "../job-search-select/job-search-select.component"; import JobSearchSelect from "../job-search-select/job-search-select.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
@@ -134,7 +134,7 @@ export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages, callback
]} ]}
name={"jobid"} name={"jobid"}
> >
<JobSearchSelect /> <JobSearchSelect notExported={false} notInvoiced={false} />
</Form.Item> </Form.Item>
</Form> </Form>
<Space> <Space>

View File

@@ -1,5 +1,5 @@
import { Button, Form, Popover, Space } from "antd"; import { Button, Form, Popover, Space } from "antd";
import React, { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -60,7 +60,7 @@ export function JobsDocumentsLocalGalleryReassign({ bodyshop, jobid, allMedia, g
]} ]}
name={"jobid"} name={"jobid"}
> >
<JobSearchSelect /> <JobSearchSelect notExported={false} notInvoiced={false}/>
</Form.Item> </Form.Item>
</Form> </Form>
<Space> <Space>

View File

@@ -19,6 +19,7 @@ import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container"; import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component"; import PrintWrapper from "../print-wrapper/print-wrapper.component";
import PartsOrderDrawer from "./parts-order-list-table-drawer.component"; import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -66,19 +67,20 @@ export function PartsOrderListTableComponent({
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : []; const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery; const { refetch } = billsQuery;
const recordActions = (record, showView = false) => ( const recordActions = (record, showView = false) => (
<Space direction="horizontal" wrap> <Space direction="horizontal" wrap>
<ShareToTeamsButton
linkText={""}
urlOverride={`${window.location.origin}/manage/jobs/${job.id}?partsorderid=${record.id}&tab=partssublet `}
/>
{showView && ( {showView && (
<Button <Button
icon={<EyeFilled />}
onClick={() => { onClick={() => {
handleOnRowClick(record); handleOnRowClick(record);
}} }}
> />
<EyeFilled />
</Button>
)} )}
<Button <Button
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid} disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => { onClick={() => {
@@ -106,6 +108,7 @@ export function PartsOrderListTableComponent({
</Button> </Button>
<Button <Button
title={t("tasks.buttons.create")} title={t("tasks.buttons.create")}
icon={<FaTasks />}
onClick={() => { onClick={() => {
setTaskUpsertContext({ setTaskUpsertContext({
context: { context: {
@@ -114,9 +117,7 @@ export function PartsOrderListTableComponent({
} }
}); });
}} }}
> />
<FaTasks />
</Button>
<Popconfirm <Popconfirm
title={t("parts_orders.labels.confirmdelete")} title={t("parts_orders.labels.confirmdelete")}
disabled={jobRO} disabled={jobRO}
@@ -137,9 +138,7 @@ export function PartsOrderListTableComponent({
}); });
}} }}
> >
<Button disabled={jobRO}> <Button disabled={jobRO} icon={<DeleteFilled />} />
<DeleteFilled />
</Button>
</Popconfirm> </Popconfirm>
<Button <Button

View File

@@ -1,9 +1,13 @@
import { Button, Input, Space, Spin } from "antd"; import { Button, Input, Space, Spin } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { ExclamationCircleFilled, ExclamationCircleOutlined } from "@ant-design/icons"; import {
ExclamationCircleFilled,
ExclamationCircleOutlined,
UserDeleteOutlined,
UsergroupDeleteOutlined
} from "@ant-design/icons";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component"; import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
@@ -19,12 +23,13 @@ export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilte
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) { export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [alertFilter, setAlertFilter] = useState(false);
const toggleAlertFilter = () => { const toggleAlertFilter = () => {
const newAlertFilter = !alertFilter; setFilter({ ...filter, alert: !filter.alert });
setAlertFilter(newAlertFilter); };
setFilter({ ...filter, alert: newAlertFilter });
const toggleUnassignedFilter = () => {
setFilter({ ...filter, unassigned: !filter.unassigned });
}; };
return ( return (
@@ -46,12 +51,19 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
allowClear allowClear
/> />
<Button <Button
type={alertFilter ? "primary" : "default"} type={filter?.alert ? "primary" : "default"}
onClick={toggleAlertFilter} onClick={toggleAlertFilter}
icon={alertFilter ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />} icon={filter?.alert ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
> >
{t("production.labels.alerts")} {t("production.labels.alerts")}
</Button> </Button>
<Button
type={filter?.unassigned ? "primary" : "default"}
onClick={toggleUnassignedFilter}
icon={filter?.unassigned ? <UserDeleteOutlined /> : <UsergroupDeleteOutlined />}
>
{t("production.labels.unassigned")}
</Button>
</Space> </Space>
); );
} }

View File

@@ -20,6 +20,8 @@ import dayjs from "../../utils/day";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component"; import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
const cardColor = (ssbuckets, totalHrs) => { const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs)); const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
@@ -417,9 +419,20 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
title={!isBodyEmpty ? headerContent : null} title={!isBodyEmpty ? headerContent : null}
extra={ extra={
!isBodyEmpty && ( !isBodyEmpty && (
<Space>
<ShareToTeamsButton
noIcon={true}
linkText={
<div className="share-to-teams-badge">
<PiMicrosoftTeamsLogo />
</div>
}
urlOverride={`${window.location.origin}/manage/jobs/${card.id}`}
/>
<Link to={{ search: `?selected=${card.id}` }}> <Link to={{ search: `?selected=${card.id}` }}>
<EyeFilled /> <EyeFilled />
</Link> </Link>
</Space>
) )
} }
> >

View File

@@ -10,6 +10,16 @@
.height-preserving-container { .height-preserving-container {
} }
.share-to-teams-badge {
background-color: #cccccc;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.react-trello-column-header { .react-trello-column-header {
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;

View File

@@ -29,7 +29,7 @@ const sortByParentId = (arr) => {
// Function to create board data based on statuses and jobs, with optional filtering // Function to create board data based on statuses and jobs, with optional filtering
export const createBoardData = ({ statuses, data, filter, cardSettings }) => { export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
const { search, employeeId, alert } = filter; const { search, employeeId, alert, unassigned } = filter;
const lanes = statuses.map((status) => ({ const lanes = statuses.map((status) => ({
id: status, id: status,
@@ -40,6 +40,13 @@ export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
let filteredJobs = let filteredJobs =
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job)); (search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
// Apply "Unassigned" filter
if (unassigned) {
filteredJobs = filteredJobs.filter(
(job) => !job.employee_body && !job.employee_prep && !job.employee_refinish && !job.employee_csr
);
}
// Filter jobs by selectedMdInsCos if it has values // Filter jobs by selectedMdInsCos if it has values
if (cardSettings?.selectedMdInsCos?.length > 0) { if (cardSettings?.selectedMdInsCos?.length > 0) {
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm)); filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd"; import { Button, Card, Col, Form, Popover, Row, Tabs } from "antd";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js"; import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js"; import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
@@ -11,6 +11,7 @@ import FilterSettings from "./FilterSettings.jsx";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { isFunction } from "lodash"; import { isFunction } from "lodash";
import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../../contexts/Notifications/notificationContext.jsx";
import { SettingOutlined } from "@ant-design/icons";
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) { function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -153,7 +154,7 @@ function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bod
return ( return (
<Popover content={overlay} open={open} placement="topRight"> <Popover content={overlay} open={open} placement="topRight">
<Button loading={loading} onClick={() => setOpen(!open)}> <Button icon={<SettingOutlined />} loading={loading} onClick={() => setOpen(!open)}>
{t("production.settings.board_settings")} {t("production.settings.board_settings")}
</Button> </Button>
</Popover> </Popover>

View File

@@ -27,6 +27,7 @@ import ProductionListColumnNote from "./production-list-columns.productionnote.c
import ProductionListColumnCategory from "./production-list-columns.status.category"; import ProductionListColumnCategory from "./production-list-columns.status.category";
import ProductionListColumnStatus from "./production-list-columns.status.component"; import ProductionListColumnStatus from "./production-list-columns.status.component";
import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component"; import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
const getEmployeeName = (employeeId, employees) => { const getEmployeeName = (employeeId, employees) => {
const employee = employees.find((e) => e.id === employeeId); const employee = employees.find((e) => e.id === employeeId);
@@ -41,7 +42,17 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
dataIndex: "viewdetail", dataIndex: "viewdetail",
key: "viewdetail", key: "viewdetail",
ellipsis: true, ellipsis: true,
render: (text, record) => <Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link> render: (text, record) => (
<Space>
<Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link>
<ShareToTeamsButton
noIcon={true}
linkText={"Share"}
noIconStyle={{ color: "#1890ff" }}
urlOverride={`${window.location.origin}/manage/jobs/${record.id}`}
/>
</Space>
)
}, },
...(Enhanced_Payroll.treatment === "on" ...(Enhanced_Payroll.treatment === "on"
? [ ? [

View File

@@ -1,5 +1,5 @@
import { Button, Dropdown } from "antd"; import { Button, Dropdown } from "antd";
import React, { useState } from "react"; import { useState } from "react";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
@@ -7,6 +7,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { PrinterFilled } from "@ant-design/icons";
const ProdTemplates = TemplateList("production"); const ProdTemplates = TemplateList("production");
const { production_by_technician_one, production_by_category_one, production_by_repair_status_one } = const { production_by_technician_one, production_by_category_one, production_by_repair_status_one } =
@@ -123,7 +124,9 @@ export function ProductionListPrint({ bodyshop }) {
return ( return (
<Dropdown trigger="click" menu={menu}> <Dropdown trigger="click" menu={menu}>
<Button loading={loading}>{t("general.labels.print")}</Button> <Button icon={<PrinterFilled />} loading={loading}>
{t("general.labels.print")}
</Button>
</Dropdown> </Dropdown>
); );
} }

View File

@@ -0,0 +1,111 @@
import PropTypes from "prop-types";
import { Button } from "antd";
import { useLocation } from "react-router-dom";
import { PiMicrosoftTeamsLogo } from "react-icons/pi";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
/**
* ShareToTeamsButton component for sharing content to Microsoft Teams via an HTTP link.
*
* This component creates a button or link that opens the Microsoft Teams share dialog with
* the provided URL, title, and message text through query parameters. The popup window is centered on the screen.
*
* @param {Object} props - The component's props.
* @param {string} [props.messageTextOverride] - Custom message text for sharing.
* @param {string} [props.urlOverride] - Custom URL to share instead of the current page's URL.
* @param {string} [props.pageTitleOverride] - Custom title for the shared page.
* @param {boolean} [props.noIcon=false] - If true, renders as a simple text link instead of a button with an icon.
* @param {Object} [props.noIconStyle={}] - Style object for the text link when noIcon is true.
* @param {Object} [props.buttonStyle={}] - Style object for the Ant Design button.
* @param {Object} [props.buttonIconStyle={}] - Style object for the icon within the button.
* @param {string} [props.linkText] - Text to display on the button or link.
* @returns {React.ReactElement} A button or text link for sharing to Microsoft Teams.
*/
const ShareToTeamsComponent = ({
bodyshop,
messageTextOverride,
urlOverride,
pageTitleOverride,
noIcon = false,
noIconStyle = {},
buttonStyle = {},
buttonIconStyle = {},
linkText
}) => {
const location = useLocation();
const { t } = useTranslation();
const currentUrl =
urlOverride ||
encodeURIComponent(`${window.location.origin}${location.pathname}${location.search}${location.hash}`);
const pageTitle =
pageTitleOverride ||
encodeURIComponent(typeof document !== "undefined" ? document.title : t("general.actions.sharetoteams"));
const messageText = messageTextOverride || encodeURIComponent(t("general.actions.sharetoteams"));
// Construct the Teams share URL with parameters
const teamsShareUrl = `https://teams.microsoft.com/share?href=${currentUrl}&preText=${messageText}&title=${pageTitle}`;
// Function to open the centered share link in a new window/tab
const handleShare = () => {
const screenWidth = window.screen.width;
const screenHeight = window.screen.height;
const windowWidth = 600;
const windowHeight = 400;
const left = screenWidth / 2 - windowWidth / 2;
const top = screenHeight / 2 - windowHeight / 2;
const windowFeatures = `width=${windowWidth},height=${windowHeight},left=${left},top=${top}`;
// noinspection JSIgnoredPromiseFromCall
window.open(teamsShareUrl, "_blank", windowFeatures);
};
// Feature is disabled
if (!bodyshop?.md_functionality_toggles?.teams) {
return null;
}
if (noIcon) {
return (
<div style={{ cursor: "pointer", ...noIconStyle }} onClick={handleShare}>
{!linkText ? t("general.actions.sharetoteams") : linkText}
</div>
);
}
return (
<Button
style={{
backgroundColor: "#6264A7",
borderColor: "#6264A7",
color: "#FFFFFF",
...buttonStyle
}}
icon={<PiMicrosoftTeamsLogo style={{ color: "#FFFFFF", ...buttonIconStyle }} />}
onClick={handleShare}
title={linkText === null ? t("general.actions.sharetoteams") : linkText}
/>
);
};
ShareToTeamsComponent.propTypes = {
messageTextOverride: PropTypes.string,
urlOverride: PropTypes.string,
pageTitleOverride: PropTypes.string,
noIcon: PropTypes.bool,
noIconStyle: PropTypes.object,
buttonStyle: PropTypes.object,
buttonIconStyle: PropTypes.object,
linkText: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
};
export default connect(mapStateToProps)(ShareToTeamsComponent);

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

View File

@@ -1,16 +1,27 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { Button, Col, Form, Input, Row, Select, Space, Switch } from "antd"; import { Button, Col, Form, Input, Row, Select, Space, Switch } from "antd";
import React, {useMemo} from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import i18n from "i18next";
const predefinedPartTypes = [ const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"];
"PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"
];
const predefinedModLbrTypes = [ const predefinedModLbrTypes = [
"LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU", "LAA",
"LA1", "LA2", "LA3", "LA4" "LAB",
"LAD",
"LAE",
"LAF",
"LAG",
"LAM",
"LAR",
"LAS",
"LAU",
"LA1",
"LA2",
"LA3",
"LA4"
]; ];
const getFieldType = (field) => { const getFieldType = (field) => {
@@ -20,24 +31,40 @@ const getFieldType = (field) => {
return null; return null;
}; };
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 }) { export default function ShopInfoPartsScan({ form }) {
const { t } = useTranslation(); const { t } = useTranslation();
const watchedFields = Form.useWatch("md_parts_scan", form); const watchedFields = Form.useWatch("md_parts_scan", form);
const operationOptions = useMemo(() => ({ const operationOptions = useMemo(
() => ({
string: [ string: [
{ label: t("bodyshop.operations.contains"), value: "contains" }, { label: t("bodyshop.operations.contains"), value: "contains" },
{ label: t("bodyshop.operations.equals"), value: "equals" }, { label: t("bodyshop.operations.equals"), value: "equals" },
{ label: t("bodyshop.operations.starts_with"), value: "startsWith" }, { label: t("bodyshop.operations.starts_with"), value: "startsWith" },
{label: t("bodyshop.operations.ends_with"), value: "endsWith"}, { label: t("bodyshop.operations.ends_with"), value: "endsWith" }
], ],
number: [ number: [
{ label: t("bodyshop.operations.equals"), value: "=" }, { label: t("bodyshop.operations.equals"), value: "=" },
{ label: t("bodyshop.operations.greater_than"), value: ">" }, { label: t("bodyshop.operations.greater_than"), value: ">" },
{label: t("bodyshop.operations.less_than"), value: "<"}, { label: t("bodyshop.operations.less_than"), value: "<" }
], ]
}), [t]); }),
[t]
);
return ( return (
<div> <div>
@@ -61,28 +88,17 @@ export default function ShopInfoPartsScan({form}) {
{ {
required: true, required: true,
message: t("general.validation.required", { message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.field"), label: t("bodyshop.fields.md_parts_scan.field")
}), })
}, }
]} ]}
> >
<Select <Select
options={[ options={fieldSelectOptions}
{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"
},
]}
onChange={() => { onChange={() => {
form.setFields([ form.setFields([
{ name: ["md_parts_scan", index, "operation"], value: "contains" }, { name: ["md_parts_scan", index, "operation"], value: "contains" },
{name: ["md_parts_scan", index, "value"], value: undefined}, { name: ["md_parts_scan", index, "value"], value: undefined }
]); ]);
}} }}
/> />
@@ -99,9 +115,9 @@ export default function ShopInfoPartsScan({form}) {
{ {
required: true, required: true,
message: t("general.validation.required", { 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]} />
@@ -119,9 +135,9 @@ export default function ShopInfoPartsScan({form}) {
{ {
required: true, required: true,
message: t("general.validation.required", { message: t("general.validation.required", {
label: t("bodyshop.fields.md_parts_scan.value"), label: t("bodyshop.fields.md_parts_scan.value")
}), })
}, }
]} ]}
> >
{fieldType === "predefined" ? ( {fieldType === "predefined" ? (
@@ -152,14 +168,65 @@ export default function ShopInfoPartsScan({form}) {
label={t("bodyshop.fields.md_parts_scan.caseInsensitive")} label={t("bodyshop.fields.md_parts_scan.caseInsensitive")}
name={[field.name, "caseInsensitive"]} name={[field.name, "caseInsensitive"]}
valuePropName="checked" valuePropName="checked"
initialValue={true}
labelCol={{ span: 14 }} labelCol={{ span: 14 }}
wrapperCol={{ span: 10 }} wrapperCol={{ span: 10 }}
> >
<Switch defaultChecked={true}/> <Switch />
</Form.Item> </Form.Item>
</Col> </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 */} {/* Actions */}
<Col span={2}> <Col span={2}>
<Space> <Space>

View File

@@ -18,6 +18,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { pageLimit } from "../../utils/config"; import { pageLimit } from "../../utils/config";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
/** /**
* Task List Component * Task List Component
@@ -266,8 +267,13 @@ function TaskListComponent({
width: "8%", width: "8%",
render: (text, record) => ( render: (text, record) => (
<Space direction="horizontal"> <Space direction="horizontal">
<ShareToTeamsButton
linkText=""
urlOverride={`${window.location.origin}/manage/tasks/alltasks?taskid=${record.id}`}
/>
<Button <Button
title={t("tasks.buttons.edit")} title={t("tasks.buttons.edit")}
icon={<EditFilled />}
onClick={() => { onClick={() => {
setTaskUpsertContext({ setTaskUpsertContext({
context: { context: {
@@ -276,18 +282,18 @@ function TaskListComponent({
} }
}); });
}} }}
> />
<EditFilled />
</Button>
<Button <Button
title={t("tasks.buttons.complete")} title={t("tasks.buttons.complete")}
onClick={() => toggleCompletedStatus(record.id, record.completed)} onClick={() => toggleCompletedStatus(record.id, record.completed)}
> icon={record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
{record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />} />
</Button>
<Button title={t("tasks.buttons.delete")} onClick={() => toggleDeletedStatus(record.id, record.deleted)}> <Button
{record.deleted ? <DeleteOutlined /> : <DeleteFilled />} title={t("tasks.buttons.delete")}
</Button> onClick={() => toggleDeletedStatus(record.id, record.deleted)}
icon={record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
/>
</Space> </Space>
) )
} }

View File

@@ -1,6 +1,6 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { Form, Modal } from "antd"; import { Form, Modal } from "antd";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -35,7 +35,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK); const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK);
const [updateTask] = useMutation(MUTATION_UPDATE_TASK); const [updateTask] = useMutation(MUTATION_UPDATE_TASK);
const { open, context } = taskUpsert; const { open, context } = taskUpsert;
const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query } = context; const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query, view } = context;
const [form] = Form.useForm(); const [form] = Form.useForm();
const [selectedJobId, setSelectedJobId] = useState(null); const [selectedJobId, setSelectedJobId] = useState(null);
const [selectedJobDetails, setSelectedJobDetails] = useState(null); const [selectedJobDetails, setSelectedJobDetails] = useState(null);
@@ -255,16 +255,17 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
} }
}; };
const taskTitle = useMemo(() => {
return existingTask ? t("tasks.actions.edit") : t("tasks.actions.new");
}, [existingTask, t]);
return ( return (
<Modal <Modal
title={<span id="task-upsert-modal-title">{taskTitle}</span>} title={
<span id="task-upsert-modal-title">
{view ? t("tasks.actions.view") : existingTask ? t("tasks.actions.edit") : t("tasks.actions.new")}
</span>
}
open={open} open={open}
okText={t("general.actions.save")} okText={t("general.actions.save")}
width="50%" width="50%"
cancelText={!isTouched ? t("general.actions.ok") : t("general.actions.cancel")}
onOk={() => { onOk={() => {
removeTaskIdFromUrl(); removeTaskIdFromUrl();
form.submit(); form.submit();
@@ -289,6 +290,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
loading={loading || (taskId && taskLoading)} loading={loading || (taskId && taskLoading)}
error={error} error={error}
data={data} data={data}
view={view}
existingTask={existingTask || taskData?.tasks_by_pk} existingTask={existingTask || taskData?.tasks_by_pk}
selectedJobId={selectedJobId} selectedJobId={selectedJobId}
setSelectedJobId={setSelectedJobId} setSelectedJobId={setSelectedJobId}

View File

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

View File

@@ -1,4 +1,5 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled, PrinterFilled } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useApolloClient, useMutation } from "@apollo/client"; import { useApolloClient, useMutation } from "@apollo/client";
import { import {
Alert, Alert,
@@ -16,32 +17,31 @@ import {
Switch, Switch,
Typography Typography
} from "antd"; } from "antd";
import { PageHeader } from "@ant-design/pro-layout"; import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
// import { useNavigate } from 'react-router-dom'; // import { useNavigate } from 'react-router-dom';
import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { useSplitTreatments } from "@splitsoftware/splitio-react";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import dayjs from "../../utils/day";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component"; import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component"; import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../../components/form-items-formatted/currency-form-item.component"; import CurrencyInput from "../../components/form-items-formatted/currency-form-item.component";
import JobCloseRoGuardContainer from "../../components/job-close-ro-guard/job-close-ro-guard.container";
import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component"; import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component"; import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component"; import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component";
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component"; import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries"; import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions.js";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import JobCloseRoGuardContainer from "../../components/job-close-ro-guard/job-close-ro-guard.container"; import dayjs from "../../utils/day";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -49,10 +49,17 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
setPrintCenterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "printCenter"
})
)
}); });
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) { export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, setPrintCenterContext }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const client = useApolloClient(); const client = useApolloClient();
@@ -171,7 +178,6 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
extra={ extra={
<Space> <Space>
<JobsCloseAutoAllocate joblines={job.joblines} form={form} disabled={!!job.date_exported || jobRO} /> <JobsCloseAutoAllocate joblines={job.joblines} form={form} disabled={!!job.date_exported || jobRO} />
<Popconfirm <Popconfirm
onConfirm={() => form.submit()} onConfirm={() => form.submit()}
disabled={jobRO} disabled={jobRO}
@@ -188,6 +194,21 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button> <Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button>
</Link> </Link>
)} )}
<Button
onClick={() => {
setPrintCenterContext({
context: {
id: job.id,
job: job,
type: "job"
}
});
}}
key="printing"
icon={<PrinterFilled />}
>
{t("jobs.actions.printCenter")}
</Button>
<JobsScoreboardAdd job={job} disabled={false} /> <JobsScoreboardAdd job={job} disabled={false} />
</Space> </Space>
} }

View File

@@ -46,7 +46,8 @@ export function AllTasksPageContainer({ setBreadcrumbs, setSelectedHeader, setTa
if (taskId) { if (taskId) {
setTaskUpsertContext({ setTaskUpsertContext({
context: { context: {
taskId taskId,
view: true
} }
}); });
urlParams.delete("taskid"); urlParams.delete("taskid");

View File

@@ -235,7 +235,12 @@ export function* signInSuccessSaga({ payload }) {
try { try {
window.$crisp.push(["set", "user:nickname", [payload.displayName || payload.email]]); 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({ InstanceRenderManager({
executeFunction: true, executeFunction: true,
args: [], args: [],
@@ -342,6 +347,9 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname }); 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) { } catch (error) {
console.error("Couldnt find $crisp."); console.error("Couldnt find $crisp.");
} }

View File

@@ -383,7 +383,10 @@
"expression": "", "expression": "",
"field": "Field", "field": "Field",
"flags": "", "flags": "",
"mark_critical": "Mark Line as Critical?",
"operation": "Operation", "operation": "Operation",
"update_field": "Field to Update",
"update_value": "Update Value",
"value": "Value" "value": "Value"
}, },
"md_payment_types": "Payment Types", "md_payment_types": "Payment Types",
@@ -686,7 +689,7 @@
"notespresets": "Notes Presets", "notespresets": "Notes Presets",
"orderstatuses": "Order Statuses", "orderstatuses": "Order Statuses",
"partslocations": "Parts Locations", "partslocations": "Parts Locations",
"partsscan": "Critical Parts Scanning", "partsscan": "Parts Scanning",
"printlater": "Print Later", "printlater": "Print Later",
"qbo": "Use QuickBooks Online?", "qbo": "Use QuickBooks Online?",
"qbo_departmentid": "QBO Department ID", "qbo_departmentid": "QBO Department ID",
@@ -707,6 +710,7 @@
"romepay": "Rome Pay", "romepay": "Rome Pay",
"scheduling": "SMART Scheduling", "scheduling": "SMART Scheduling",
"scoreboardsetup": "Scoreboard Setup", "scoreboardsetup": "Scoreboard Setup",
"shop_enabled_features": "Shop Enabled Features",
"shopinfo": "Shop Information", "shopinfo": "Shop Information",
"speedprint": "Speed Print Configuration", "speedprint": "Speed Print Configuration",
"ssbuckets": "Job Size Definitions", "ssbuckets": "Job Size Definitions",
@@ -729,6 +733,11 @@
"save": "Shop configuration saved successfully. ", "save": "Shop configuration saved successfully. ",
"unsavedchanges": "Unsaved changes will be lost. Are you sure you want to continue?" "unsavedchanges": "Unsaved changes will be lost. Are you sure you want to continue?"
}, },
"tooltips": {
"md_parts_scan": {
"update_value_tooltip": "Some fields require coded values in order to function properly (e.g. labor and part types). Please reach out to support if you have any questions."
}
},
"validation": { "validation": {
"centermustexist": "The chosen responsibility center does not exist.", "centermustexist": "The chosen responsibility center does not exist.",
"larsplit": "Refinish hour split must add up to 1.", "larsplit": "Refinish hour split must add up to 1.",
@@ -969,10 +978,10 @@
"productiondollars": "Total Dollars in Production", "productiondollars": "Total Dollars in Production",
"productionhours": "Total Hours in Production", "productionhours": "Total Hours in Production",
"projectedmonthlysales": "Projected Monthly Sales", "projectedmonthlysales": "Projected Monthly Sales",
"scheduledindate": "Sheduled In Today: {{date}}", "scheduledindate": "Scheduled In Today: {{date}}",
"scheduledintoday": "Sheduled In Today", "scheduledintoday": "Scheduled In Today",
"scheduledoutdate": "Sheduled Out Today: {{date}}", "scheduledoutdate": "Scheduled Out Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today", "scheduledouttoday": "Scheduled Out Today",
"tasks": "Tasks" "tasks": "Tasks"
} }
}, },
@@ -1176,6 +1185,7 @@
"edit": "Edit", "edit": "Edit",
"login": "Login", "login": "Login",
"next": "Next", "next": "Next",
"ok": "Ok",
"previous": "Previous", "previous": "Previous",
"print": "Print", "print": "Print",
"refresh": "Refresh", "refresh": "Refresh",
@@ -1190,6 +1200,7 @@
"send": "Send", "send": "Send",
"sendbysms": "Send by SMS", "sendbysms": "Send by SMS",
"senderrortosupport": "Send Error to Support", "senderrortosupport": "Send Error to Support",
"sharetoteams": "Share to Teams",
"submit": "Submit", "submit": "Submit",
"tryagain": "Try Again", "tryagain": "Try Again",
"view": "View", "view": "View",
@@ -1525,7 +1536,7 @@
"addDocuments": "Add Job Documents", "addDocuments": "Add Job Documents",
"addNote": "Add Note", "addNote": "Add Note",
"addtopartsqueue": "Add to Parts Queue", "addtopartsqueue": "Add to Parts Queue",
"addtoproduction": "Add In Production Flag", "addtoproduction": "Add to Production",
"addtoscoreboard": "Add to Scoreboard", "addtoscoreboard": "Add to Scoreboard",
"allocate": "Allocate", "allocate": "Allocate",
"autoallocate": "Auto Allocate", "autoallocate": "Auto Allocate",
@@ -1568,7 +1579,7 @@
"printCenter": "Print Center", "printCenter": "Print Center",
"recalculate": "Recalculate", "recalculate": "Recalculate",
"reconcile": "Reconcile", "reconcile": "Reconcile",
"removefromproduction": "Remove In Production Flag", "removefromproduction": "Remove from Production",
"schedule": "Schedule", "schedule": "Schedule",
"sendcsi": "Send CSI", "sendcsi": "Send CSI",
"sendpartspricechange": "Send Parts Price Change", "sendpartspricechange": "Send Parts Price Change",
@@ -2855,6 +2866,7 @@
"tasks": "Tasks", "tasks": "Tasks",
"totalhours": "Total Hrs ", "totalhours": "Total Hrs ",
"touchtime": "T/T", "touchtime": "T/T",
"unassigned": "Unassigned",
"vertical": "Vertical", "vertical": "Vertical",
"viewname": "View Name", "viewname": "View Name",
"wide": "Wide" "wide": "Wide"
@@ -3183,7 +3195,8 @@
"tasks": { "tasks": {
"actions": { "actions": {
"edit": "Edit Task", "edit": "Edit Task",
"new": "New Task" "new": "New Task",
"view": "View Task"
}, },
"buttons": { "buttons": {
"allTasks": "All", "allTasks": "All",

View File

@@ -383,7 +383,10 @@
"expression": "", "expression": "",
"field": "", "field": "",
"flags": "", "flags": "",
"mark_critical": "",
"operation": "", "operation": "",
"update_field": "",
"update_value": "",
"value": "" "value": ""
}, },
"md_payment_types": "", "md_payment_types": "",
@@ -707,6 +710,7 @@
"romepay": "", "romepay": "",
"scheduling": "", "scheduling": "",
"scoreboardsetup": "", "scoreboardsetup": "",
"shop_enabled_features": "",
"shopinfo": "", "shopinfo": "",
"speedprint": "", "speedprint": "",
"ssbuckets": "", "ssbuckets": "",
@@ -729,6 +733,11 @@
"save": "", "save": "",
"unsavedchanges": "" "unsavedchanges": ""
}, },
"tooltips": {
"md_parts_scan": {
"update_value_tooltip": ""
}
},
"validation": { "validation": {
"centermustexist": "", "centermustexist": "",
"larsplit": "", "larsplit": "",
@@ -1176,6 +1185,7 @@
"edit": "Editar", "edit": "Editar",
"login": "", "login": "",
"next": "", "next": "",
"ok": "",
"previous": "", "previous": "",
"print": "", "print": "",
"refresh": "", "refresh": "",
@@ -1190,6 +1200,7 @@
"send": "", "send": "",
"sendbysms": "", "sendbysms": "",
"senderrortosupport": "", "senderrortosupport": "",
"sharetoteams": "",
"submit": "", "submit": "",
"tryagain": "", "tryagain": "",
"view": "", "view": "",
@@ -2855,6 +2866,7 @@
"tasks": "", "tasks": "",
"totalhours": "", "totalhours": "",
"touchtime": "", "touchtime": "",
"unassigned": "",
"vertical": "", "vertical": "",
"viewname": "", "viewname": "",
"wide": "" "wide": ""
@@ -3183,7 +3195,8 @@
"tasks": { "tasks": {
"actions": { "actions": {
"edit": "", "edit": "",
"new": "" "new": "",
"view": ""
}, },
"buttons": { "buttons": {
"allTasks": "", "allTasks": "",

View File

@@ -383,7 +383,10 @@
"expression": "", "expression": "",
"field": "", "field": "",
"flags": "", "flags": "",
"mark_critical": "",
"operation": "", "operation": "",
"update_field": "",
"update_value": "",
"value": "" "value": ""
}, },
"md_payment_types": "", "md_payment_types": "",
@@ -707,6 +710,7 @@
"romepay": "", "romepay": "",
"scheduling": "", "scheduling": "",
"scoreboardsetup": "", "scoreboardsetup": "",
"shop_enabled_features": "",
"shopinfo": "", "shopinfo": "",
"speedprint": "", "speedprint": "",
"ssbuckets": "", "ssbuckets": "",
@@ -729,6 +733,11 @@
"save": "", "save": "",
"unsavedchanges": "" "unsavedchanges": ""
}, },
"tooltips": {
"md_parts_scan": {
"update_value_tooltip": ""
}
},
"validation": { "validation": {
"centermustexist": "", "centermustexist": "",
"larsplit": "", "larsplit": "",
@@ -1176,6 +1185,7 @@
"edit": "modifier", "edit": "modifier",
"login": "", "login": "",
"next": "", "next": "",
"ok": "",
"previous": "", "previous": "",
"print": "", "print": "",
"refresh": "", "refresh": "",
@@ -1190,6 +1200,7 @@
"send": "", "send": "",
"sendbysms": "", "sendbysms": "",
"senderrortosupport": "", "senderrortosupport": "",
"sharetoteams": "",
"submit": "", "submit": "",
"tryagain": "", "tryagain": "",
"view": "", "view": "",
@@ -2855,6 +2866,7 @@
"tasks": "", "tasks": "",
"totalhours": "", "totalhours": "",
"touchtime": "", "touchtime": "",
"unassigned": "",
"vertical": "", "vertical": "",
"viewname": "", "viewname": "",
"wide": "" "wide": ""
@@ -3183,7 +3195,8 @@
"tasks": { "tasks": {
"actions": { "actions": {
"edit": "", "edit": "",
"new": "" "new": "",
"view": ""
}, },
"buttons": { "buttons": {
"allTasks": "", "allTasks": "",

View File

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

View File

@@ -1,6 +1,6 @@
import { gql } from "@apollo/client";
import { Kind, parse, print, visit } from "graphql"; import { Kind, parse, print, visit } from "graphql";
import client from "./GraphQLClient"; import client from "./GraphQLClient";
import { gql } from "@apollo/client";
/* eslint-disable no-loop-func */ /* eslint-disable no-loop-func */
@@ -114,9 +114,10 @@ export function printQuery(query) {
* @param templateQueryToExecute * @param templateQueryToExecute
* @param templateObject * @param templateObject
* @param useShopSpecificTemplate * @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 // Advanced Filtering and Sorting modifications start here
// Parse the query and apply the filters and sorters // Parse the query and apply the filters and sorters
@@ -147,7 +148,7 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
contextData = data; 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 { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr"; import InstanceRenderManager from "./src/utils/instanceRenderMgr";
import chalk from "chalk"; 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", { process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
timeZone: "America/Los_Angeles" timeZone: "America/Los_Angeles"
}); });
@@ -22,7 +22,7 @@ export const logger = createLogger("info", {
export default defineConfig({ export default defineConfig({
base: "/", base: "/",
plugins: [ plugins: [
//visualizer(), // Ensure all plugins are Vite 6 compatible
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })), ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
VitePWA({ VitePWA({
injectRegister: "auto", injectRegister: "auto",
@@ -31,14 +31,12 @@ export default defineConfig({
short_name: InstanceRenderManager({ short_name: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE, instance: process.env.VITE_APP_INSTANCE,
imex: "ImEX Online", imex: "ImEX Online",
rome: "Rome Online", rome: "Rome Online"
}), }),
name: InstanceRenderManager({ name: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE, instance: process.env.VITE_APP_INSTANCE,
imex: "ImEX Online", imex: "ImEX Online",
rome: "Rome Online", rome: "Rome Online"
}), }),
description: "The ultimate bodyshop management system.", description: "The ultimate bodyshop management system.",
icons: [ icons: [
@@ -46,7 +44,7 @@ export default defineConfig({
src: InstanceRenderManager({ src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE, instance: process.env.VITE_APP_INSTANCE,
imex: "favicon.png", imex: "favicon.png",
rome: "ro-favicon.png", rome: "ro-favicon.png"
}), }),
sizes: "64x64 32x32 24x24 16x16", sizes: "64x64 32x32 24x24 16x16",
type: "image/x-icon" type: "image/x-icon"
@@ -55,7 +53,7 @@ export default defineConfig({
src: InstanceRenderManager({ src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE, instance: process.env.VITE_APP_INSTANCE,
imex: "logo192.png", imex: "logo192.png",
rome: "logo192.png", rome: "logo192.png"
}), }),
type: "image/png", type: "image/png",
sizes: "192x192" sizes: "192x192"
@@ -64,7 +62,7 @@ export default defineConfig({
src: InstanceRenderManager({ src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE, instance: process.env.VITE_APP_INSTANCE,
imex: "logo512.png", imex: "logo512.png",
rome: "ro-favicon.png", rome: "ro-favicon.png"
}), }),
type: "image/png", type: "image/png",
sizes: "512x512" sizes: "512x512"
@@ -73,7 +71,7 @@ export default defineConfig({
theme_color: InstanceRenderManager({ theme_color: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE, instance: process.env.VITE_APP_INSTANCE,
imex: "#1890ff", imex: "#1890ff",
rome: "#fff", rome: "#fff"
}), }),
background_color: "#fff", background_color: "#fff",
gcm_sender_id: "103953800507" gcm_sender_id: "103953800507"
@@ -204,8 +202,10 @@ export default defineConfig({
"react-redux" "react-redux"
], ],
esbuildOptions: { esbuildOptions: {
// Update for Vite 6: Use proper file extensions
loader: { loader: {
".js": "jsx" ".jsx": "jsx",
".tsx": "tsx"
} }
} }
}, },

View File

@@ -39,3 +39,11 @@
headers: headers:
- name: event-secret - name: event-secret
value_from_env: EVENT_SECRET value_from_env: EVENT_SECRET
- name: Rome Usage Report
webhook: '{{HASURA_API_URL}}/data/usagereport'
schedule: 0 12 * * 5
include_in_metadata: true
payload: {}
headers:
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH

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

View File

@@ -55,7 +55,7 @@ exports.default = async (req, res) => {
const csv = converter.json2csv(shopList, { emptyFieldValue: "" }); const csv = converter.json2csv(shopList, { emptyFieldValue: "" });
emailer emailer
.sendTaskEmail({ .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")}`, subject: `RO Usage Report - ${moment().format("MM/DD/YYYY")}`,
text: ` text: `
Usage Report for ${moment().format("MM/DD/YYYY")} for Rome Online Customers. 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>`, imex: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
rome: `Rome Online API - ${process.env.NODE_ENV} <noreply@romeonline.io>` rome: `Rome Online API - ${process.env.NODE_ENV} <noreply@romeonline.io>`
}), }),
to: ["patrick@imexsystems.ca", "support@thinkimex.com"], to: ["support@thinkimex.com"],
subject: subject, subject: subject,
text: text, text: text,
ses: { ses: {
@@ -92,7 +92,7 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
}, },
(err, info) => { (err, info) => {
// (message, type, user, record, meta // (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) { } catch (error) {
@@ -239,24 +239,24 @@ const emailBounce = async (req, res) => {
return; return;
} }
//If it's bounced, log it as bounced in audit log. Send an email to the user. //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, sesid: messageId,
status: "Bounced", status: "Bounced",
context: message.bounce?.bouncedRecipients context: message.bounce?.bouncedRecipients
}); });
mailer.sendMail( mailer.sendMail(
{ {
from: InstanceMgr({ from: InstanceManager({
imex: `ImEX Online <noreply@imex.online>`, imex: `ImEX Online <noreply@imex.online>`,
rome: `Rome Online <noreply@romeonline.io>` rome: `Rome Online <noreply@romeonline.io>`
}), }),
to: replyTo, to: replyTo,
//bcc: "patrick@snapt.ca", //bcc: "patrick@snapt.ca",
subject: `${InstanceMgr({ subject: `${InstanceManager({
imex: "ImEX Online", imex: "ImEX Online",
rome: "Rome Online" rome: "Rome Online"
})} Bounced Email - RE: ${subject}`, })} Bounced Email - RE: ${subject}`,
text: `${InstanceMgr({ text: `${InstanceManager({
imex: "ImEX Online", imex: "ImEX Online",
rome: "Rome Online" rome: "Rome Online"
})} has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. })} 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) => { (err, info) => {
logger.log("sns-error", err ? "error" : "debug", "api", null, { 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 mod_lb_hrs
oem_partno oem_partno
alt_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}) { notcritical: update_joblines(where: {id: {_nin: $IdsToMarkCritical}, jobid: {_eq: $jobid}}, _set: {critical: false}) {
affected_rows affected_rows
} }
}` }`;
exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) { exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
associations(where: {active: {_eq: true}, useremail: {_eq: $user}}) { 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!) { exports.STATUS_UPDATE = `query STATUS_UPDATE($period: timestamptz!, $today: timestamptz!) {
bodyshops(where: { created_at: { _gte: $period } }) { bodyshops(where: { created_at: { _gte: $period } }) {
shopname 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 queries = require("../graphql-client/queries");
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const predefinedPartTypes = [ const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"];
"PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG",
];
const predefinedModLbrTypes = [ const predefinedModLbrTypes = [
"LAA", "LAB", "LAD", "LAE", "LAF", "LAG", "LAM", "LAR", "LAS", "LAU", "LAA",
"LA1", "LA2", "LA3", "LA4", "LAB",
"LAD",
"LAE",
"LAF",
"LAG",
"LAM",
"LAR",
"LAS",
"LAU",
"LA1",
"LA2",
"LA3",
"LA4"
]; ];
exports.partsScan = async function (req, res) { exports.partsScan = async function (req, res) {
console.log('hello')
const { jobid } = req.body; const { jobid } = req.body;
const BearerToken = req.BearerToken; const BearerToken = req.BearerToken;
const client = req.userGraphQLClient; 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); logger.log("job-parts-scan", "DEBUG", req.user?.email, jobid, null);
try { try {
const data = await client.setHeaders({ Authorization: BearerToken }).request( const data = await client
queries.QUERY_PARTS_SCAN, .setHeaders({ Authorization: BearerToken })
{ id: jobid } .request(queries.QUERY_PARTS_SCAN, { id: jobid });
);
const rules = data.jobs_by_pk.bodyshop.md_parts_scan || []; const rules = data.jobs_by_pk.bodyshop.md_parts_scan || [];
if (!Array.isArray(rules)) { if (!Array.isArray(rules)) {
@@ -36,21 +44,22 @@ exports.partsScan = async function (req, res) {
? new RegExp(rule.value, "i") ? new RegExp(rule.value, "i")
: typeof rule.value === "string" : typeof rule.value === "string"
? new RegExp(rule.value) ? new RegExp(rule.value)
: null, : null
})); }));
const criticalIds = new Set(); const criticalIds = new Set();
for (const jobline of data.jobs_by_pk.joblines) { for (const jobline of data.jobs_by_pk.joblines) {
for (const { field, regex, operation, value } of compiledRules) { for (const { field, regex, operation, value, mark_critical, update_field, update_value } of compiledRules) {
if (criticalIds.has(jobline.id)) break; // Skip further evaluation if already critical // 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 jobValue = jobline[field];
let match = false; let match = false;
if (field === "part_number") { if (field === "part_number") {
match = regex 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; : jobline.oem_partno === value || jobline.alt_partno === value;
} else if (field === "part_type") { } else if (field === "part_type") {
match = predefinedPartTypes.includes(value) && value === jobValue; match = predefinedPartTypes.includes(value) && value === jobValue;
@@ -68,22 +77,44 @@ exports.partsScan = async function (req, res) {
} }
if (match) { if (match) {
if (mark_critical) {
//Could technically lead to duplicates, but they'd only be n positives, ultimately leading a positive.
criticalIds.add(jobline.id); criticalIds.add(jobline.id);
break; // No need to evaluate further rules for this jobline }
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( const result = await client
queries.UPDATE_PARTS_CRITICAL, .setHeaders({ Authorization: BearerToken })
{ IdsToMarkCritical: Array.from(criticalIds), jobid } .request(queries.UPDATE_PARTS_CRITICAL, { IdsToMarkCritical: Array.from(criticalIds), jobid });
);
res.status(200).json(result); res.status(200).json(result);
} catch (error) { } catch (error) {
logger.log("job-parts-scan-error", "ERROR", req.user?.email, jobid, { logger.log("job-parts-scan-error", "ERROR", req.user?.email, jobid, {
jobid, jobid,
error: error.message, error: error.message,
stack: error.stack
}); });
res.status(400).json(JSON.stringify({ message: error?.message })); 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 { Canvas, FontLibrary } = require("skia-canvas");
const Chart = require("chart.js/auto"); const Chart = require("chart.js/auto");
@@ -65,7 +64,7 @@ const getChartConfiguration = (keys, values, override) => {
return defaultsDeep(override || {}, defaultConfiguration); return defaultsDeep(override || {}, defaultConfiguration);
}; };
const processCanvasRequest = async (req, res, isSkia = false) => { const processCanvasRequest = async (req, res) => {
const { logger } = req; const { logger } = req;
const { w, h, values, keys, override } = req.body; 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); const configuration = getChartConfiguration(keys, values, override);
// Placeholders to allow fine control over GAC
let canvas = null; let canvas = null;
let ctx = null; let ctx = null;
let chart = null; let chart = null;
@@ -85,16 +83,15 @@ const processCanvasRequest = async (req, res, isSkia = false) => {
try { try {
// Create the canvas // Create the canvas
canvas = isSkia ? new Canvas(width, height) : createCanvas(width, height); canvas = new Canvas(width, height);
ctx = canvas.getContext("2d"); ctx = canvas.getContext("2d");
// Render the chart // Render the chart
chart = new Chart(ctx, configuration); chart = new Chart(ctx, configuration);
// Generate and send the image // Generate and send the image
chartImage = isSkia ? (await canvas.toBuffer("image/png")).toString("base64") : canvas.toDataURL(); chartImage = (await canvas.toBuffer("image/png")).toString("base64");
res.status(200).send(`data:image/png;base64,${chartImage}`);
res.status(200).send(isSkia ? `data:image/png;base64,${chartImage}` : chartImage);
} catch (error) { } catch (error) {
// Log the error and send the response // Log the error and send the response
logger.log("canvas-error", "error", "jsr", null, { error: error.message }); logger.log("canvas-error", "error", "jsr", null, { error: error.message });
@@ -104,27 +101,27 @@ const processCanvasRequest = async (req, res, isSkia = false) => {
if (chart) { if (chart) {
chart.destroy(); chart.destroy();
} }
ctx = null; // Explicitly nullify for garbage collection ctx = null;
canvas = null; // Explicitly nullify for garbage collection canvas = null;
chartImage = null; chartImage = null;
} }
}; };
const enqueueRequest = (req, res, isSkia) => { const enqueueRequest = (req, res) => {
if (requestQueue.length >= CANVAS_QUEUE_LIMIT) { if (requestQueue.length >= CANVAS_QUEUE_LIMIT) {
res.status(503).send("Server is busy. Please try again later."); res.status(503).send("Server is busy. Please try again later.");
return false; return false;
} }
requestQueue.push({ req, res, isSkia }); requestQueue.push({ req, res });
req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length }); req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
return true; return true;
}; };
const processNextInQueue = async () => { const processNextInQueue = async () => {
while (requestQueue.length > 0) { while (requestQueue.length > 0) {
const { req, res, isSkia } = requestQueue.shift(); const { req, res } = requestQueue.shift();
try { try {
await processCanvasRequest(req, res, isSkia); await processCanvasRequest(req, res);
} catch (err) { } catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message }); 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) => { exports.canvas = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res, false)) return; if (isProcessing || !enqueueRequest(req, res)) 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;
isProcessing = true; isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message })); 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 router = express.Router();
const { inlinecss } = require("../render/inlinecss"); const { inlinecss } = require("../render/inlinecss");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { canvas, canvasSkia } = require("../render/canvas-handler"); const { canvas } = require("../render/canvas-handler");
const validateCanvasInputMiddleware = require("../middleware/validateCanvasInputMiddleware"); const validateCanvasInputMiddleware = require("../middleware/validateCanvasInputMiddleware");
// Define the route for inline CSS rendering // Define the route for inline CSS rendering
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss); router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss);
router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvas); router.post("/canvas-skia", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
router.post("/canvas-skia", [validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware], canvasSkia); router.post("/canvas", validateFirebaseIdTokenMiddleware, validateCanvasInputMiddleware, canvas);
module.exports = router; module.exports = router;