Compare commits

...

133 Commits

Author SHA1 Message Date
Allan Carr
b8836c7ae1 Merged in feature/IO-3030-QBO-Payment-Private-Note (pull request #1934)
IO-3030 Null Check memo
2024-11-18 16:40:15 +00:00
Allan Carr
eca31c5618 IO-3030 Null Check memo
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-18 08:42:52 -08:00
Allan Carr
7140b8d585 Merged in feature/IO-3031-Appointment-Schedule-View-Day (pull request #1931)
IO-3031 Appointment Schedule View Day
2024-11-16 01:33:29 +00:00
Allan Carr
5eed8d9809 IO-3031 Appointment Schedule View Day
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-15 17:35:49 -08:00
Allan Carr
f266ee1cfe Merged in feature/IO-3028-Word-Wrap-Line-Description (pull request #1928)
IO-3028 Adjust to TextArea with autoSize
2024-11-15 20:54:44 +00:00
Allan Carr
9550de5131 IO-3028 Adjust to TextArea with autoSize
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-15 12:57:08 -08:00
Patrick Fic
1f76ff882c Remove IO Event Logging. 2024-11-15 11:03:10 -08:00
Patrick Fic
749f73a272 Merged in feature/IO-2920-cash-discounting (pull request #1927)
IO-2920 Update config & totals for discount.
2024-11-15 18:58:54 +00:00
Patrick Fic
9c1774c417 Merge branch 'release/2024-11-15' into feature/IO-2920-cash-discounting 2024-11-15 10:58:22 -08:00
Allan Carr
26b3a43ce5 Merge branch 'feature/IO-3027-Datapumps-Refactor' into release/2024-11-15
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>

# Conflicts:
#	.vscode/settings.json
2024-11-15 10:05:08 -08:00
Allan Carr
78678dd3dc IO-3027 Datapumps Refactor
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-15 10:04:03 -08:00
Allan Carr
9dc4546b2e Merged in feature/IO-3033-Total-Loss-Indicator (pull request #1925)
IO-3033 Total Loss Indicator

Approved-by: Dave Richer
2024-11-15 17:47:56 +00:00
Allan Carr
95aa0e45a6 IO-3033 Total Loss Indicator
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-14 16:47:35 -08:00
Allan Carr
ce9a77efcf IO-3027 Datapumps Refactor
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-14 16:15:17 -08:00
Dave Richer
e9e1e820a7 release/2024-11-15 - Expose S3 client through createS3Client
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-14 11:57:40 -08:00
Allan Carr
b027a4e618 IO-3031 Adjust prop
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-14 11:52:47 -08:00
Allan Carr
c7fc75aa5c Merged in feature/IO-3031-Appointment-Schedule-View-Day (pull request #1922)
IO-3031 View Day when Scheduling

Approved-by: Dave Richer
2024-11-14 19:50:34 +00:00
Allan Carr
98d2372daf Merged in feature/IO-3030-QBO-Payment-Private-Note (pull request #1920)
IO-3030 QBO Payment Private Note

Approved-by: Dave Richer
2024-11-14 19:40:20 +00:00
Allan Carr
bf51380167 IO-3031 View Day when Scheduling
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-14 11:19:09 -08:00
Dave Richer
1ec827097f Merged in feature/IO-3029-Enhanced-Logging-File-Based (pull request #1921)
feature/IO-3029-Enhanced-Logging-File-Based: Adjust XML and JSON log to always upload
2024-11-14 18:55:57 +00:00
Dave Richer
89fabf85e1 feature/IO-3029-Enhanced-Logging-File-Based: Adjust XML and JSON log to always upload
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-14 10:55:14 -08:00
Allan Carr
ff7dd7d3ea IO-3030 QBO Payment Private Note
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-14 10:37:08 -08:00
Dave Richer
8cc4f88fa7 Merged in feature/IO-3029-Enhanced-Logging-File-Based (pull request #1918)
feature/IO-3029-Enhanced-Logging-File-Based: Final Enhancements
2024-11-14 16:36:13 +00:00
Dave Richer
2439755f9e feature/IO-3029-Enhanced-Logging-File-Based: Final Enhancements
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-14 08:34:21 -08:00
Dave Richer
7e6ab3a5ff Merged in feature/IO-3029-Enhanced-Logging-File-Based (pull request #1916)
feature/IO-3029-Enhanced-Logging-File-Based: Update Stream Key name
2024-11-14 04:14:54 +00:00
Dave Richer
763384f05f feature/IO-3029-Enhanced-Logging-File-Based: Update Stream Key name
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-13 20:14:17 -08:00
Dave Richer
34f876f838 Merged in feature/IO-3029-Enhanced-Logging-File-Based (pull request #1914)
feature/IO-3029-Enhanced-Logging-File-Based: Add File based S3 Logging.
2024-11-14 03:57:24 +00:00
Dave Richer
cba2da8da7 feature/IO-3029-Enhanced-Logging-File-Based: Add fix bugs
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-13 13:09:37 -08:00
Dave Richer
f3d8aa3438 feature/IO-3029-Enhanced-Logging-File-Based: Add File based S3 Logging.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-13 12:59:56 -08:00
Allan Carr
2f3eccf3d8 Merged in feature/IO-3028-Word-Wrap-Line-Description (pull request #1912)
IO-3028 Extend to Notes
2024-11-13 18:50:32 +00:00
Allan Carr
2b3e64d607 IO-3028 Extend to Notes
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-13 10:49:47 -08:00
Allan Carr
05b20505bb Merged in feature/IO-3028-Word-Wrap-Line-Description (pull request #1910)
IO-3028 Word Wrap Line Description

Approved-by: Dave Richer
2024-11-13 18:17:34 +00:00
Allan Carr
bddeae945c IO-3028 Word Wrap Line Description
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-13 10:06:01 -08:00
Patrick Fic
5b267f03b9 Add additional GIN indexes for db. 2024-11-12 20:21:43 -08:00
Dave Richer
357d916e0a Merged in release/2024-11-15 (pull request #1908)
[DO NOT MERGE] - Release/2024 11 15

Approved-by: Patrick Fic
2024-11-13 00:30:22 +00:00
Dave Richer
6ed12ebe7d Merged in feature/IO-3026-Enhanced-Notifications (pull request #1909)
feature/IO-3026-Enhanced-Notifications - final revisions
2024-11-12 22:52:13 +00:00
Dave Richer
6703bc025d feature/IO-3026-Enhanced-Notifications - final revisions
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-12 14:51:50 -08:00
Dave Richer
387dac6779 Merged in feature/IO-3026-Enhanced-Notifications (pull request #1906)
Feature/IO-3026 Enhanced Notifications
2024-11-12 22:23:35 +00:00
Dave Richer
6f454dd4cb feature/IO-3026-Enhanced-Notifications - final revisions
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-12 14:20:49 -08:00
Dave Richer
1440a60228 feature/IO-3026-Enhanced-Notifications - Initial commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-12 12:31:46 -08:00
Allan Carr
f2aa3960aa Merged in release/2024-11-08 (pull request #1905)
Release/2024 11 08 IO-2921 IO-3025
2024-11-09 08:37:27 +00:00
Allan Carr
06508f3ad8 Merged in feature/IO-2921-CARSTAR-Canada-Chatter-Integration (pull request #1903)
Feature/IO-2921 CARSTAR Canada Chatter Integration
2024-11-09 08:33:32 +00:00
Allan Carr
8d4195b596 IO-2921 Adjust SFTP setup
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-09 00:35:18 -08:00
Allan Carr
9e190e7fb7 Merged in feature/IO-3025-Autohouse-Datapump-Refactor (pull request #1902)
IO-3025 Adjust for promise and change processing
2024-11-09 08:32:49 +00:00
Allan Carr
5cbf00b0c8 IO-3025 Adjust for promise and change processing
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-09 00:32:51 -08:00
Allan Carr
655aeb86fc IO-2921 Adjust for Promise and change processing
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-08 23:43:18 -08:00
Allan Carr
225549275d Merged in release/2024-11-08 (pull request #1901)
Release/2024 11 08 IO-2921 IO-3025
2024-11-09 06:33:14 +00:00
Allan Carr
f0717b8b36 IO-2921 Shift Email outside of Batch
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-08 22:30:22 -08:00
Allan Carr
78771ae750 IO-3025 Shift Email send to outside of batch
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-08 22:28:02 -08:00
Allan Carr
0389908398 Merged in feature/IO-2921-CARSTAR-Canada-Chatter-Integration (pull request #1899)
IO-2921 Shift Email outside of Batch
2024-11-09 06:27:57 +00:00
Allan Carr
54bee763df Merged in feature/IO-3025-Autohouse-Datapump-Refactor (pull request #1898)
IO-3025 Shift Email send to outside of batch
2024-11-09 06:25:39 +00:00
Dave Richer
1117a94930 Merged in release/2024-11-08 (pull request #1897)
release/2024-11-08 - Small fix to font script
2024-11-09 05:21:03 +00:00
Dave Richer
5fbfb992c7 release/2024-11-08 - Small fix to font script
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-08 21:18:58 -08:00
Dave Richer
87b3b65f3e Merged in release/2024-11-08 (pull request #1893)
Release/2024-11-08 into master-AIO - IO-2921, IO-2969, IO-3001, IO-3015, IO-3017, IO-3018, IO-3025

Approved-by: Allan Carr
2024-11-09 04:52:01 +00:00
Allan Carr
9970190909 Merged in feature/IO-3025-Autohouse-Datapump-Refactor (pull request #1894)
IO-3025 Autohouse Datapump Refactor
2024-11-09 03:31:37 +00:00
Allan Carr
8eee371a90 IO-3025 Autohouse Datapump Refactor
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-08 19:30:33 -08:00
Dave Richer
ba97b1efef Merged in feature/IO-3015-addl-prod-indexes (pull request #1891)
Feature/IO-3015 addl prod indexes
2024-11-08 17:57:47 +00:00
Allan Carr
8d8887c28e Merged in feature/IO-3017-Lifecycle-Average-Time (pull request #1889)
IO-3017 Lifecycle Average Time

Approved-by: Dave Richer
2024-11-08 17:33:31 +00:00
Dave Richer
3b19432974 feature/IO-3017-Lifecycle-Average-Time - Small fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-08 09:32:58 -08:00
Allan Carr
a14b2340b0 IO-3017 Lifecycle NaN prevention
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-08 09:24:58 -08:00
Allan Carr
624f8e77cb IO-3017 Lifecycle Average Time
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-07 13:56:53 -08:00
Dave Richer
fb624c817d Merged in feature/IO-2969-Fonts-For-Production (pull request #1888)
Feature/IO-2969 Fonts For Production into release
2024-11-07 20:42:26 +00:00
Dave Richer
c2b4b66ed1 hotfix/IO-2969-Fonts-For-Production
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-07 12:40:13 -08:00
Dave Richer
ffec03ab6c Merge remote-tracking branch 'origin/master-AIO' into hotfix/IO-2969-Fonts-For-Production 2024-11-07 12:37:01 -08:00
Allan Carr
552163d7b9 IO-2921 Upload directory
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-06 15:53:51 -08:00
Allan Carr
db1f59578c Merged in feature/IO-2921-CARSTAR-Canada-Chatter-Integration (pull request #1887)
IO-2921 Re-factor as batch and get docker compose dev working for sftp testing

Approved-by: Dave Richer
2024-11-06 23:51:46 +00:00
Allan Carr
8ec5831ec5 IO-2921 Re-factor as batch and get docker compose dev working for sftp testing
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-06 15:42:14 -08:00
Allan Carr
0146ac5b7b Merged in feature/IO-3018-QBO-Standarize-name (pull request #1886)
IO-3018 QBO Standardize name

Approved-by: Dave Richer
2024-11-06 16:37:47 +00:00
Allan Carr
a603e5c0b8 Merged in feature/IO-2921-CARSTAR-Canada-Chatter-Integration (pull request #1885)
IO-2921 Adjustment for SFTP Private Key

Approved-by: Dave Richer
2024-11-06 16:36:52 +00:00
Patrick Fic
9aab47d8f8 IO-2920 Update config & totals for discount. 2024-11-05 16:37:21 -08:00
Patrick Fic
f2f84e2da8 Merge branch 'master-AIO' into feature/IO-2920-cash-discounting 2024-11-05 16:00:47 -08:00
Allan Carr
338906e288 IO-3018 QBO Standardize name
Trim StandardizeName

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-05 11:53:42 -08:00
Allan Carr
542997b1a7 IO-2921 Adjustment for SFTP Private Key
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-05 11:02:18 -08:00
Dave Richer
5fce548666 Merged in release/2024-11-01 (pull request #1884)
Release/2024-11-01 into master-AIO - IO-2921, IO-3006, IO-3008, IO-3009, IO-3010
2024-11-02 15:14:06 +00:00
Dave Richer
80322caad0 release/2024-11-01 - Update Trigger for job_updated - Make the callback work with old and new Hasura
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-02 08:11:22 -07:00
Patrick Fic
56472d24d9 IO-3015 add additional indexs. 2024-11-01 21:33:03 -07:00
Patrick Fic
db5dcc271d IO-30015 add new indexes to production. 2024-11-01 20:35:44 -07:00
Allan Carr
73ab02225e IO-2921 Adjustment to getting Secret
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-01 17:55:33 -07:00
Allan Carr
83a1b7690d Merged in feature/IO-2921-CARSTAR-Canada-Chatter-Integration (pull request #1881)
IO-2921 Adjustment to getting Secret
2024-11-02 00:54:56 +00:00
Allan Carr
c9e28b1ed2 Merge branch 'master-AIO' into feature/IO-2921-CARSTAR-Canada-Chatter-Integration
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-01 17:52:54 -07:00
Allan Carr
c25c66d00f Merged in feature/IO-3009-Clear-Dates (pull request #1879)
IO-3009 Correction for nulls
2024-11-01 17:16:23 +00:00
Allan Carr
d319ab49d4 IO-3009 Correction for nulls
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-01 10:18:14 -07:00
Allan Carr
a069989ea7 Merged in feature/IO-3014-Timeticket-UI-Sort (pull request #1876)
IO-3014 Change Polling Intervals

Approved-by: Dave Richer
2024-10-31 18:13:51 +00:00
Dave Richer
8e3aa186cb Merged in hotfix/2024-10-31-Database-Issues (pull request #1877)
Hotfix/2024 10 31 Database Issues into master-AIO - IO-3012 IO-3009 IO-3014
2024-10-31 18:06:08 +00:00
Allan Carr
01c55d6277 IO-3014 Change Polling Intervals
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-10-31 11:04:05 -07:00
Dave Richer
3438907d8d Merge remote-tracking branch 'origin/feature/IO-3014-Timeticket-UI-Sort' into hotfix/2024-10-31-Database-Issues 2024-10-31 11:02:09 -07:00
Allan Carr
ae020b651e IO-3014 Further Query Refinements for T/T
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-10-31 10:56:05 -07:00
Allan Carr
d22988df15 Merged in feature/IO-3014-Timeticket-UI-Sort (pull request #1875)
IO-3014 TimeTicket UI Sort

Approved-by: Dave Richer
2024-10-31 17:54:36 +00:00
Dave Richer
8136a56ad2 Merge remote-tracking branch 'origin/feature/IO-3014-Timeticket-UI-Sort' into hotfix/2024-10-31-Database-Issues 2024-10-31 10:53:56 -07:00
Allan Carr
830f6c0eea IO-3014 TimeTicket UI Sort
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-10-31 10:48:40 -07:00
Dave Richer
4c1849289a Merge remote-tracking branch 'origin/feature/IO-3014-Timeticket-UI-Sort' into hotfix/2024-10-31-Database-Issues 2024-10-31 10:46:21 -07:00
Dave Richer
c45a4780e3 Merge remote-tracking branch 'origin/feature/IO-3012-Remove-Sort-for-SB-TimeTickets-Query' into hotfix/2024-10-31-Database-Issues 2024-10-31 10:34:56 -07:00
Allan Carr
d4adc4c1aa Merged in feature/IO-3009-Clear-Dates (pull request #1872)
IO-3009 Clear Dates

Approved-by: Dave Richer
2024-10-31 17:06:38 +00:00
Allan Carr
d9e71423f5 Merged in feature/IO-3010-Task-Table-UI-Mods (pull request #1873)
IO-3010 Task Table UI refactor

Approved-by: Dave Richer
2024-10-31 17:06:02 +00:00
Allan Carr
6cac0f9594 IO-3010 Task Table UI refactor
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-10-30 17:38:09 -07:00
Allan Carr
2ab4615642 IO-3009 Clear Dates
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-10-30 12:48:27 -07:00
Dave Richer
dd5961d419 release/2024-11-01 - Update Trigger for job_updated
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-30 12:07:50 -07:00
Allan Carr
8190958ba3 Merged in feature/IO-3012-Remove-Sort-for-SB-TimeTickets-Query (pull request #1871)
IO-3012 Remove Sort from SB Timeticket Query

Approved-by: Dave Richer
2024-10-30 19:03:18 +00:00
Allan Carr
77e009f316 IO-3012 Remove Sort from SB Timeticket Query
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-10-30 11:19:46 -07:00
Dave Richer
2b2738a8d1 Merge branch 'release/2024-11-01' of bitbucket.org:snaptsoft/bodyshop into release/2024-11-01 2024-10-30 09:38:56 -07:00
Dave Richer
3d10c9da7f release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-30 09:38:27 -07:00
Allan Carr
e82c77d119 Merged in feature/IO-3008-Save-&-New-Time-Ticket (pull request #1868)
IO-3008 Save and New Flat Rate value

Approved-by: Dave Richer
2024-10-30 16:12:21 +00:00
Allan Carr
855a78be05 Merged in feature/IO-3006-CDK-PBS-Error-Log-INSERT_EXPORT_LOG (pull request #1867)
IO-3006 CDK PBS Error Log on INSERT_EXPORT_LOG

Approved-by: Dave Richer
2024-10-30 16:11:49 +00:00
Dave Richer
a29e840797 release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-30 09:09:16 -07:00
Allan Carr
1b30c1ab58 IO-3008 Save and New Flat Rate value
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-10-29 20:03:58 -07:00
Allan Carr
80f235f12e IO-3006 CDK PBS Error Log on INSERT_EXPORT_LOG
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-10-29 13:33:40 -07:00
Dave Richer
9b67148522 Merged in release/2024-11-01 (pull request #1866)
Release/2024-11-01 into master-AIO - IO-2979 - Misc Logger related fixes
2024-10-29 20:10:02 +00:00
Dave Richer
6b501e4619 release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-29 11:02:08 -07:00
Dave Richer
42f1d6fa13 release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-29 10:53:09 -07:00
Dave Richer
3f247a9227 release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-29 09:54:45 -07:00
Dave Richer
63b914731b release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-29 08:25:54 -07:00
Dave Richer
23f8f69bbe release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-29 08:19:58 -07:00
Dave Richer
fc3ea2bdf8 release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-29 07:37:40 -07:00
Dave Richer
96e970faf7 release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 18:16:48 -07:00
Dave Richer
c133195607 release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 17:48:43 -07:00
Dave Richer
d75ea2b1a6 release/2024-11-01 - Misc fixes
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 17:38:46 -07:00
Dave Richer
ec0fd840e4 Merged in feature/IO-2979-DST (pull request #1856)
IO-2979-DST into release/2024-11-01 - DST Related Fixes
2024-10-28 21:05:57 +00:00
Dave Richer
971a81fc27 feature/IO-2979-DST - Finish DST Stuff
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 14:04:14 -07:00
Dave Richer
19050d31f7 Merged in release/2024-11-01 (pull request #1853)
Release/2024-11-01 into master-AIO  - Fix Logging Issues
2024-10-28 20:39:28 +00:00
Dave Richer
e605433379 Merge remote-tracking branch 'origin/release/2024-11-01' into feature/IO-2979-DST 2024-10-28 13:10:08 -07:00
Dave Richer
b9ebb70b7a release/2024-11-01 - Remove Trace Log Level
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 13:06:47 -07:00
Dave Richer
79ed6f2388 release/2024-11-01 - Remove Trace Log Level
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 12:24:40 -07:00
Dave Richer
785449a986 release/2024-11-01 - Remove Trace Log Level
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 12:03:48 -07:00
Dave Richer
0b7d469e0e feature/IO-2979-DST - Finish DST
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 11:55:13 -07:00
Dave Richer
a57156756e feature/IO-2979-DST - Normalize usages of dayjs to dayjs not day, move locale hook
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 11:03:30 -07:00
Dave Richer
c4c30d98d4 release/2024-11-01 - Adjust hostname check
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 10:07:10 -07:00
Dave Richer
d4e8803b13 release/2024-11-01 - Adjust client body buffer size
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 09:47:27 -07:00
Dave Richer
1f2786ddec release/2024-11-01 - Fix some log things
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-28 09:31:37 -07:00
Dave Richer
e90cda07e4 Merged in release/2024-10-25 (pull request #1848)
Release/2024-10-25 - IO-2921, IO-2966, IO-2973, IO-2974, IO-2978, IO-2992, IO-2996, IO-2998
2024-10-26 04:48:05 +00:00
Dave Richer
9793daa04c release/2024-10-25 - Add The local email viewer to the reference folder.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-10-25 11:58:50 -07:00
Allan Carr
117ced8fe7 Merged in feature/IO-2973-Created-By-Tasks (pull request #1846)
IO-2973 Created By Tasks

Approved-by: Dave Richer
2024-10-25 18:53:11 +00:00
Dave Richer
5b00ded5f6 hotfix/IO-2969-Fonts-For-Production - Register fonts
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-27 19:29:26 -04:00
Dave Richer
c5b19d8f22 hotfix/IO-2969-Fonts-For-Production - Register fonts
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-09-27 19:20:19 -04:00
113 changed files with 15479 additions and 11964 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text eol=lf

View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Install required packages
dnf install -y fontconfig freetype
# Move to the /tmp directory for temporary download and extraction
cd /tmp
# Download the Montserrat font zip file
wget https://images.imex.online/fonts/montserrat.zip -O montserrat.zip
# Unzip the downloaded font file
unzip montserrat.zip -d montserrat
# Move the font files to the system fonts directory
mv montserrat/montserrat/*.ttf /usr/share/fonts
# Rebuild the font cache
fc-cache -fv
# Clean up
rm -rf /tmp/montserrat /tmp/montserrat.zip
echo "Montserrat fonts installed and cached successfully."

View File

@@ -1 +1,2 @@
client_max_body_size 50M;
client_max_body_size 50M;
client_body_buffer_size 5M;

30
.vscode/settings.json vendored
View File

@@ -8,5 +8,35 @@
"pattern": "**/IMEX.xml",
"systemId": "logs/IMEX.xsd"
}
],
"cSpell.words": [
"antd",
"appointmentconfirmation",
"appt",
"autohouse",
"autohouseid",
"billlines",
"bodyshop",
"bodyshopid",
"bodyshops",
"CIECA",
"claimscorp",
"claimscorpid",
"Dinero",
"driveable",
"IMEX",
"imexshopid",
"jobid",
"joblines",
"Kaizen",
"labhrs",
"larhrs",
"mixdata",
"ownr",
"promanager",
"shopname",
"smartscheduling",
"timetickets",
"touchtime"
]
}

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,7 @@
This will connect to your dockers local stack session and render the email in HTML.
```shell
node index.js
```
http://localhost:3334

View File

@@ -0,0 +1,116 @@
// index.js
import express from 'express';
import fetch from 'node-fetch';
import {simpleParser} from 'mailparser';
const app = express();
const PORT = 3334;
app.get('/', async (req, res) => {
try {
const response = await fetch('http://localhost:4566/_aws/ses');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
const messagesHtml = await parseMessages(data.messages);
res.send(renderHtml(messagesHtml));
} catch (error) {
console.error('Error fetching messages:', error);
res.status(500).send('Error fetching messages');
}
});
async function parseMessages(messages) {
const parsedMessages = await Promise.all(
messages.map(async (message, index) => {
try {
const parsed = await simpleParser(message.RawData);
return `
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: lightgray">
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: white">
<div class="mb-2">
<span class="font-bold text-lg">Message ${index + 1}</span>
</div>
<div class="mb-2">
<span class="font-semibold">From:</span> ${message.Source}
</div>
<div class="mb-2">
<span class="font-semibold">Region:</span> ${message.Region}
</div>
<div class="mb-2">
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
</div>
</div>
<div class="prose">
${parsed.html || parsed.textAsHtml || 'No HTML content available'}
</div>
</div>
`;
} catch (error) {
console.error('Error parsing email:', error);
return `
<div class="bg-white shadow-md rounded-lg p-4 mb-6">
<div class="mb-2">
<span class="font-bold text-lg">Message ${index + 1}</span>
</div>
<div class="mb-2">
<span class="font-semibold">From:</span> ${message.Source}
</div>
<div class="mb-2">
<span class="font-semibold">Region:</span> ${message.Region}
</div>
<div class="mb-2">
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
</div>
<div class="text-red-500">
Error parsing email content
</div>
</div>
`;
}
})
);
return parsedMessages.join('');
}
function renderHtml(messagesHtml) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Messages Viewer</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
background-color: #f3f4f6;
font-family: Arial, sans-serif;
}
.container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.prose {
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container bg-white shadow-lg rounded-lg p-6">
<h1 class="text-2xl font-bold text-center mb-6">Email Messages Viewer</h1>
<div id="messages-container">
${messagesHtml}
</div>
</div>
</body>
</html>
`;
}
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"name": "localemailviewer",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.1",
"mailparser": "^3.7.1",
"node-fetch": "^3.3.2"
}
}

View File

@@ -11156,6 +11156,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>imexpay</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>insurancecos</name>
<definition_loaded>false</definition_loaded>
@@ -11198,27 +11219,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>intellipay</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>intellipay_cash_discount</name>
<definition_loaded>false</definition_loaded>
@@ -11747,6 +11747,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ttl_adjustment</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>ttl_tax_adjustment</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -11775,6 +11817,27 @@
</concept_node>
</children>
</folder_node>
<concept_node>
<name>romepay</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>scheduling</name>
<definition_loaded>false</definition_loaded>
@@ -36253,6 +36316,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_cust_payable_cash_discount</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>total_repairs</name>
<definition_loaded>false</definition_loaded>
@@ -48360,6 +48444,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>tasks_in_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>
<concept_node>
<name>tasks_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48402,6 +48507,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_amount_in_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>
<concept_node>
<name>total_amount_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48444,6 +48570,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_hours_in_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>
<concept_node>
<name>total_hours_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48465,6 +48612,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_jobs_in_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>
<concept_node>
<name>total_jobs_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48507,6 +48675,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lab_in_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>
<concept_node>
<name>total_lab_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48549,6 +48738,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lar_in_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>
<concept_node>
<name>total_lar_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48724,6 +48934,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>tasks_in_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>
<concept_node>
<name>tasks_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48766,6 +48997,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_amount_in_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>
<concept_node>
<name>total_amount_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48808,6 +49060,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_hours_in_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>
<concept_node>
<name>total_hours_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48829,6 +49102,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_jobs_in_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>
<concept_node>
<name>total_jobs_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48871,6 +49165,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lab_in_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>
<concept_node>
<name>total_lab_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -48913,6 +49228,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total_lar_in_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>
<concept_node>
<name>total_lar_on_board</name>
<definition_loaded>false</definition_loaded>
@@ -51761,6 +52097,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_not_production_status</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>production_over_time</name>
<definition_loaded>false</definition_loaded>
@@ -54225,6 +54582,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>created_by</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>description</name>
<definition_loaded>false</definition_loaded>
@@ -54487,6 +54865,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>related_items</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>remind_at</name>
<definition_loaded>false</definition_loaded>

View File

@@ -2,8 +2,6 @@ import { ApolloProvider } from "@apollo/client";
import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day";
import "dayjs/locale/en";
import React from "react";
import { useTranslation } from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
@@ -19,8 +17,6 @@ if (import.meta.env.DEV) {
Userpilot.initialize("NX-69145f08");
}
dayjs.locale("en");
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,

View File

@@ -45,7 +45,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
if (fcmToken) {
setpollInterval(0);
} else {
setpollInterval(60000);
setpollInterval(90000);
}
}, [fcmToken]);

View File

@@ -1,10 +1,10 @@
import { Card, Table, Tag } from "antd";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import { useTranslation } from "react-i18next";
import React, { useEffect, useState } from "react";
import dayjs from "../../../utils/day";
import DashboardRefreshRequired from "../refresh-required.component";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import dayjs from "../../../utils/day";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import DashboardRefreshRequired from "../refresh-required.component";
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
@@ -46,6 +46,11 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
dataIndex: "humanReadable",
key: "humanReadable"
},
{
title: t("job_lifecycle.columns.average_human_readable"),
dataIndex: "averageHumanReadable",
key: "averageHumanReadable"
},
{
title: t("job_lifecycle.columns.status_count"),
key: "statusCount",

View File

@@ -40,13 +40,11 @@ export function DmsLogEvents({ socket, logs, bodyshop }) {
function LogLevelHierarchy(level) {
switch (level) {
case "TRACE":
return "pink";
case "DEBUG":
return "orange";
case "INFO":
return "blue";
case "WARNING":
case "WARN":
return "yellow";
case "ERROR":
return "red";

View File

@@ -8,7 +8,7 @@ import { INSERT_EULA_ACCEPTANCE } from "../../graphql/user.queries";
import { useMutation } from "@apollo/client";
import { acceptEula } from "../../redux/user/user.actions";
import { useTranslation } from "react-i18next";
import day from "../../utils/day";
import dayjs from "../../utils/day";
import "./eula.styles.scss";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
@@ -208,7 +208,7 @@ const EulaFormComponent = ({ form, handleChange, onFinish, t }) => (
{
required: true,
validator: (_, value) => {
if (day(value).isSame(day(), "day")) {
if (dayjs(value).isSame(dayjs(), "day")) {
return Promise.resolve();
}
return Promise.reject(new Error(t("eula.messages.date_accepted")));

View File

@@ -1,22 +1,40 @@
import { DatePicker } from "antd";
import { DatePicker, Space, TimePicker } from "antd";
import PropTypes from "prop-types";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
import dayjs from "../../utils/day";
import { fuzzyMatchDate } from "./formats.js";
const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, isDateOnly = false, ...restProps }) => {
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const DateTimePicker = ({
value,
onChange,
onBlur,
id,
onlyFuture,
onlyToday,
isDateOnly = false,
isSeparatedTime = false,
bodyshop,
...restProps
}) => {
const [isManualInput, setIsManualInput] = useState(false);
const { t } = useTranslation();
const handleChange = useCallback(
(newDate) => {
if (onChange) {
onChange(newDate || null);
onChange(bodyshop?.timezone && newDate ? dayjs(newDate).tz(bodyshop.timezone, true) : newDate);
}
setIsManualInput(false);
},
[onChange]
[onChange, bodyshop?.timezone]
);
const handleBlur = useCallback(
@@ -70,24 +88,57 @@ const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, is
return (
<div onKeyDown={handleKeyDown} id={id} style={{ width: "100%" }}>
<DatePicker
showTime={
isDateOnly
? false
: {
format: "hh:mm a",
minuteStep: 15,
defaultValue: dayjs(dayjs(), "HH:mm:ss")
}
}
format={isDateOnly ? "MM/DD/YYYY" : "MM/DD/YYYY hh:mm a"}
value={value ? dayjs(value) : null}
onChange={handleChange}
placeholder={isDateOnly ? t("general.labels.date") : t("general.labels.datetime")}
onBlur={onBlur || handleBlur}
disabledDate={handleDisabledDate}
{...restProps}
/>
{isSeparatedTime && (
<Space direction="vertical" style={{ width: "100%" }}>
<DatePicker
showTime={false}
format="MM/DD/YYYY"
value={value ? dayjs(value) : null}
onChange={handleChange}
placeholder={t("general.labels.date")}
onBlur={handleBlur}
disabledDate={handleDisabledDate}
isDateOnly={true}
{...restProps}
/>
{value && (
<TimePicker
format="hh:mm a"
minuteStep={15}
defaultOpenValue={dayjs(value)
.hour(dayjs().hour())
.minute(Math.floor(dayjs().minute() / 15) * 15)
.second(0)}
onChange={(value) => {
handleChange(value);
onBlur();
}}
placeholder={t("general.labels.time")}
{...restProps}
/>
)}
</Space>
)}
{!isSeparatedTime && (
<DatePicker
showTime={
isDateOnly
? false
: {
format: "hh:mm a",
minuteStep: 15,
defaultValue: dayjs(dayjs(), "HH:mm:ss")
}
}
format={isDateOnly ? "MM/DD/YYYY" : "MM/DD/YYYY hh:mm a"}
value={value ? dayjs(value) : null}
onChange={handleChange}
placeholder={isDateOnly ? t("general.labels.date") : t("general.labels.datetime")}
onBlur={onBlur || handleBlur}
disabledDate={handleDisabledDate}
{...restProps}
/>
)}
</div>
);
};
@@ -99,7 +150,8 @@ DateTimePicker.propTypes = {
id: PropTypes.string,
onlyFuture: PropTypes.bool,
onlyToday: PropTypes.bool,
isDateOnly: PropTypes.bool
isDateOnly: PropTypes.bool,
isSeparatedTime: PropTypes.bool
};
export default React.memo(DateTimePicker);
export default connect(mapStateToProps, null)(DateTimePicker);

View File

@@ -116,18 +116,15 @@ function Header({
const { t } = useTranslation();
const deleteBetaCookie = () => {
const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`));
if (cookieExists) {
const domain = window.location.hostname.split(".").slice(-2).join(".");
document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
console.log(`betaSwitchImex cookie deleted`);
} else {
console.log(`betaSwitchImex cookie does not exist`);
}
};
deleteBetaCookie();
// const deleteBetaCookie = () => {
// const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`));
// if (cookieExists) {
// const domain = window.location.hostname.split(".").slice(-2).join(".");
// document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
// }
// };
//
// deleteBetaCookie();
const accountingChildren = [];

View File

@@ -118,8 +118,7 @@ export function JobLinesComponent({
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {})
}
}),
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
},
{
title: t("joblines.fields.oem_partno"),

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from "react";
import day from "../../utils/day";
import dayjs from "../../utils/day";
import axios from "axios";
import { Badge, Card, Space, Table, Tag } from "antd";
import { gql, useQuery } from "@apollo/client";
@@ -72,7 +72,7 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
dataIndex: "start",
key: "start",
render: (text) => DateTimeFormatterFunction(text),
sorter: (a, b) => day(a.start).unix() - day(b.start).unix()
sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix()
},
{
title: t("job_lifecycle.columns.relative_start"),
@@ -90,7 +90,7 @@ export function JobLifecycleComponent({ job, statuses, ...rest }) {
}
return isEmpty(a.end) ? 1 : -1;
}
return day(a.end).unix() - day(b.end).unix();
return dayjs(a.end).unix() - dayjs(b.end).unix();
},
render: (text) => (isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text))
},

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useMutation } from "@apollo/client";
import { Button, Form, notification, Popover, Select, Space } from "antd";
import day from "../../utils/day";
import dayjs from "../../utils/day";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -48,7 +48,7 @@ export function JobLineDispatchButton({
const result = await dispatchLines({
variables: {
partsDispatch: {
dispatched_at: day(),
dispatched_at: dayjs(),
employeeid: values.employeeid,
jobid: job.id,
dispatched_by: currentUser.email,
@@ -138,7 +138,11 @@ export function JobLineDispatchButton({
return (
<Popover open={visible} content={popMenu}>
<Button disabled={selectedLines.length === 0 || jobRO || disabled} loading={loading} onClick={() => setVisible(true)}>
<Button
disabled={selectedLines.length === 0 || jobRO || disabled}
loading={loading}
onClick={() => setVisible(true)}
>
{t("joblines.actions.dispatchparts", { count: selectedLines.length })}
</Button>
</Popover>

View File

@@ -45,7 +45,8 @@ export default function JobLineNotePopup({ jobline, disabled }) {
if (editing)
return (
<div>
<Input
<Input.TextArea
autoSize
autoFocus
suffix={loading ? <LoadingSpinner /> : null}
value={note}

View File

@@ -1,10 +1,10 @@
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Form, Input, InputNumber, Modal, Select, Switch } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import InputCurrency from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JoblinesPreset from "../job-lines-preset-button/job-lines-preset-button.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -61,7 +61,7 @@ export function JobLinesUpsertModalComponent({ bodyshop, open, jobLine, handleCa
]}
name="line_desc"
>
<Input />
<Input.TextArea autoSize />
</Form.Item>
<JoblinesPreset form={form} />
</LayoutFormRow>

View File

@@ -141,14 +141,16 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
key: t("jobs.fields.ded_amt"),
total: job.job_totals.totals.custPayable.deductible
},
...(InstanceRenderManager({
imex: [{
key: t("jobs.fields.federal_tax_payable"),
total: job.job_totals.totals.custPayable.federal_tax
}],
...InstanceRenderManager({
imex: [
{
key: t("jobs.fields.federal_tax_payable"),
total: job.job_totals.totals.custPayable.federal_tax
}
],
rome: [],
promanager: "USE_ROME"
})),
}),
{
key: t("jobs.fields.other_amount_payable"),
total: job.job_totals.totals.custPayable.other_customer_amount
@@ -158,11 +160,32 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
total: job.job_totals.totals.custPayable.dep_taxes
},
{
key: t("jobs.labels.total_cust_payable"),
total: job.job_totals.totals.custPayable.total,
bold: true
},
...(bodyshop.intellipay_config?.enable_cash_discount
? [
{
key: t("jobs.labels.total_cust_payable_cash_discount"),
total: job.job_totals.totals.custPayable.total,
bold: true
},
{
key: t("jobs.labels.total_cust_payable"),
total: Dinero(job.job_totals.totals.custPayable.total)
.add(
Dinero(job.job_totals.totals.custPayable.total).percentage(
bodyshop.intellipay_config?.cash_discount_percentage || 0
)
)
.toJSON(),
bold: true
}
]
: [
{
key: t("jobs.labels.total_cust_payable"),
total: job.job_totals.totals.custPayable.total,
bold: true
}
]),
{
key: t("jobs.labels.net_repairs"),
total: job.job_totals.totals.net_repairs,

View File

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -12,7 +13,6 @@ import Car from "../job-damage-visual/job-damage-visual.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 FormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -185,6 +185,9 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
<Form.Item label={t("jobs.fields.towin")} name="towin" valuePropName="checked">
<Switch disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.tlos_ind")} name="tlos_ind" valuePropName="checked">
<Switch disabled={jobRO} />
</Form.Item>
</FormRow>
</Col>
<Col {...lossColDamage}>

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, notification, Row, Table } from "antd";
import day from "../../utils/day";
import dayjs from "../../utils/day";
import React from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_PARTS_DISPATCH_LINE } from "../../graphql/parts-dispatch.queries";
@@ -11,7 +11,7 @@ export default function PartsDispatchExpander({ dispatch, job }) {
const [updateDispatchLine] = useMutation(UPDATE_PARTS_DISPATCH_LINE);
const handleAccept = async ({ partsDispatchLineId }) => {
const accepted_at = day();
const accepted_at = dayjs();
const result = await updateDispatchLine({
variables: { id: partsDispatchLineId, line: { accepted_at } },
optimisticResponse: {

View File

@@ -120,14 +120,6 @@ var formats = {
};
const localizer = (dayjsLib) => {
// load dayjs plugins
dayjsLib.extend(isBetween);
dayjsLib.extend(isSameOrAfter);
dayjsLib.extend(isSameOrBefore);
dayjsLib.extend(localeData);
dayjsLib.extend(localizedFormat);
dayjsLib.extend(minMax);
dayjsLib.extend(utc);
var locale = function locale(dj, c) {
return c ? dj.locale(c) : dj;
};
@@ -136,7 +128,8 @@ const localizer = (dayjsLib) => {
// then use the timezone aware version
//TODO This was the issue entirely...
// var dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib;
// var dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib;
var dayjs = dayjsLib;
function getTimezoneOffset(date) {

View File

@@ -20,6 +20,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
problemJobs: selectProblemJobs
});
const localizer = local(dayjs);
export function ScheduleCalendarWrapperComponent({

View File

@@ -1,6 +1,5 @@
import { Button, Col, Form, Input, Row, Select, Space, Switch, Typography } from "antd";
import axios from "axios";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -8,13 +7,14 @@ import { createStructuredSelector } from "reselect";
import { calculateScheduleLoad } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import EmailInput from "../form-items-formatted/email-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
import "./schedule-job-modal.scss";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -84,7 +84,7 @@ export function ScheduleJobModalComponent({
}
]}
>
<DateTimePicker onBlur={handleDateBlur} onlyFuture />
<DateTimePicker onBlur={handleDateBlur} onlyFuture isSeparatedTime />
</Form.Item>
<Form.Item
name="scheduled_completion"

View File

@@ -4,12 +4,12 @@ import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component";
import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component";
import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component";
import { useApolloClient, useQuery } from "@apollo/client";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_BLOCKED_DAYS, QUERY_SCOREBOARD } from "../../graphql/scoreboard.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import dayjs from "../../utils/day";
import { useApolloClient, useQuery } from "@apollo/client";
import { GET_BLOCKED_DAYS, QUERY_SCOREBOARD } from "../../graphql/scoreboard.queries";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -26,7 +26,7 @@ export function ScoreboardDisplayComponent({ bodyshop }) {
start: dayjs().startOf("month"),
end: dayjs().endOf("month")
},
pollInterval: 60000
pollInterval: 60000*5
});
const { data } = scoreboardSubscription;

View File

@@ -1,13 +1,13 @@
import { useQuery } from "@apollo/client";
import { Col, Row } from "antd";
import _ from "lodash";
import dayjs from "../../utils/day";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_TIME_TICKETS_IN_RANGE_SB } from "../../graphql/timetickets.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import dayjs from "../../utils/day";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
@@ -86,7 +86,7 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) {
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
pollInterval: 60000,
pollInterval: 60000*5,
skip: !fixedPeriods
});

View File

@@ -1,11 +1,11 @@
import { useQuery } from "@apollo/client";
import { Col, Row } from "antd";
import _ from "lodash";
import dayjs from "../../utils/day";
import queryString from "query-string";
import React, { useMemo } from "react";
import { useLocation } from "react-router-dom";
import { QUERY_TIME_TICKETS_IN_RANGE_SB } from "../../graphql/timetickets.queries";
import dayjs from "../../utils/day";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
@@ -68,7 +68,7 @@ export default function ScoreboardTimeTickets() {
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
pollInterval: 60000,
pollInterval: 60000*5,
skip: !fixedPeriods
});

View File

@@ -142,7 +142,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
rome: [
{
key: "intellipay",
label: t("bodyshop.labels.intellipay"),
label: InstanceRenderManager({ rome: t("bodyshop.labels.romepay"), imex: t("bodyshop.labels.imexpay") }),
children: <ShopInfoIntellipay form={form} />
}
],

View File

@@ -676,7 +676,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
}
]}
>
<Input.TextArea rows={3} />
<Input.TextArea autoSize />
</Form.Item>
<Space wrap>
<DeleteFilled
@@ -737,7 +737,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
}
]}
>
<Input.TextArea rows={3} />
<Input.TextArea autoSize />
</Form.Item>
<Space wrap>
<DeleteFilled
@@ -1187,7 +1187,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<Input />
<Input.TextArea autoSize />
</Form.Item>
<Form.Item
label={t("joblines.fields.mod_lbr_ty")}
@@ -1330,7 +1330,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
}
]}
>
<Input />
<Input.TextArea autoSize />
</Form.Item>
<Space wrap>

View File

@@ -39,14 +39,13 @@ export function ShopInfoIntellipay({ bodyshop, form }) {
</Form.Item>
<Form.Item
label={t("bodyshop.fields.intellipay_config.cash_discount_percentage")}
valuePropName="checked"
dependencies={[["intellipay_config", "enable_cash_discount"]]}
name={["intellipay_config", "cash_discount_percentage"]}
rules={[
({ getFieldsValue }) => ({ required: form.getFieldValue(["intellipay_config", "enable_cash_discount"]) })
]}
>
<InputNumber min={0} max={100} precision={1} suffix='%'/>
<InputNumber min={0} max={100} precision={1} suffix="%" />
</Form.Item>
</LayoutFormRow>
</>

View File

@@ -144,7 +144,7 @@ function TaskListComponent({
title: t("tasks.fields.created_by"),
dataIndex: "created_by",
key: "created_by",
width: "10%",
width: "8%",
defaultSortOrder: "descend",
sorter: true,
sortOrder: sortcolumn === "created_by" && sortorder,
@@ -166,65 +166,70 @@ function TaskListComponent({
});
}
if (showRo) {
columns.push({
title: t("tasks.fields.job.ro_number"),
dataIndex: ["job", "ro_number"],
key: "job.ro_number",
width: "8%",
render: (text, record) =>
record.job ? (
<Link to={`/manage/jobs/${record.job.id}?tab=tasks`}>{record.job.ro_number || t("general.labels.na")}</Link>
) : (
t("general.labels.na")
)
});
}
columns.push({
title: t("tasks.fields.related_items"),
key: "related_items",
width: "12%",
render: (text, record) => {
const items = [];
// Job
if (showRo && record.job) {
items.push(
<Link key="job" to={`/manage/jobs/${record.job.id}?tab=tasks`}>
{t("tasks.fields.job.ro_number")}: {record.job.ro_number}
</Link>
);
}
if (showRo && !record.job) {
items.push(`${t("tasks.fields.job.ro_number")}: ${t("general.labels.na")}`);
}
// Jobline
if (record.jobline?.line_desc) {
items.push(
<span key="jobline">
{t("tasks.fields.jobline")}: {record.jobline.line_desc}
</span>
);
}
// Parts Order
if (record.parts_order) {
const { order_number, vendor } = record.parts_order;
const partsOrderText =
order_number && vendor?.name ? `${order_number} - ${vendor.name}` : t("general.labels.na");
items.push(
<Link
key="parts_order"
to={`/manage/jobs/${record.job.id}?partsorderid=${record.parts_order.id}&tab=partssublet`}
>
{t("tasks.fields.parts_order")}: {partsOrderText}
</Link>
);
}
// Bill
if (record.bill) {
const { invoice_number, vendor } = record.bill;
const billText = invoice_number && vendor?.name ? `${invoice_number} - ${vendor.name}` : t("general.labels.na");
items.push(
<Link key="bill" to={`/manage/jobs/${record.job.id}?billid=${record.bill.id}&tab=partssublet`}>
{t("tasks.fields.bill")}: {billText}
</Link>
);
}
return items.length > 0 ? <Space direction="vertical">{items}</Space> : null;
}
});
columns.push(
{
title: t("tasks.fields.jobline"),
dataIndex: ["jobline", "id"],
key: "jobline.id",
width: "8%",
render: (text, record) => record?.jobline?.line_desc || ""
},
{
title: t("tasks.fields.parts_order"),
dataIndex: ["parts_order", "id"],
key: "part_order.id",
width: "8%",
render: (text, record) =>
record.parts_order ? (
<Link to={`/manage/jobs/${record.job.id}?partsorderid=${record.parts_order.id}&tab=partssublet`}>
{record.parts_order.order_number && record.parts_order.vendor && record.parts_order.vendor.name
? `${record.parts_order.order_number} - ${record.parts_order.vendor.name}`
: t("general.labels.na")}
</Link>
) : (
""
)
},
{
title: t("tasks.fields.bill"),
dataIndex: ["bill", "id"],
key: "bill.id",
width: "10%",
render: (text, record) =>
record.bill ? (
<Link to={`/manage/jobs/${record.job.id}?billid=${record.bill.id}&tab=partssublet`}>
{record.bill.invoice_number && record.bill.vendor && record.bill.vendor.name
? `${record.bill.invoice_number} - ${record.bill.vendor.name}`
: t("general.labels.na")}
</Link>
) : (
""
)
},
{
title: t("tasks.fields.title"),
dataIndex: "title",
key: "title",
minWidth: "20%",
sorter: true,
sortOrder: sortcolumn === "title" && sortorder
},
@@ -258,7 +263,7 @@ function TaskListComponent({
{
title: t("tasks.fields.actions"),
key: "toggleCompleted",
width: "5%",
width: "8%",
render: (text, record) => (
<Space direction="horizontal">
<Button

View File

@@ -39,7 +39,7 @@ export function TimeTicketList({
extra
}) {
const [state, setState] = useState({
sortedInfo: {},
sortedInfo: { columnKey: 'date', order: 'descend' },
filteredInfo: { text: "" }
});

View File

@@ -1,7 +1,7 @@
import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, Modal, notification, Space } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import dayjs from "../../utils/day";
import { useMutation, useQuery } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Form, Modal, notification, Space } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -11,9 +11,9 @@ import { INSERT_NEW_TIME_TICKET, UPDATE_TIME_TICKET } from "../../graphql/timeti
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectTimeTicket } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import TimeTicketModalComponent from "./time-ticket-modal.component";
import dayjs from "../../utils/day";
import TimeTicketsCommitToggleComponent from "../time-tickets-commit-toggle/time-tickets-commit-toggle.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import TimeTicketModalComponent from "./time-ticket-modal.component";
const mapStateToProps = createStructuredSelector({
timeTicketModal: selectTimeTicket,
@@ -87,7 +87,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
if (enterAgain) {
//Capture the existing information and repopulate it.
const prev = form.getFieldsValue(["date", "employeeid"]);
const prev = form.getFieldsValue(["date", "employeeid", "flat_rate"]);
form.resetFields();

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import day from "../../utils/day";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -29,7 +29,7 @@ export function TimeTicketsCommit({ bodyshop, currentUser, timeticket, disabled,
? { commited_by: null, committed_at: null }
: {
commited_by: currentUser.email,
committed_at: day()
committed_at: dayjs()
};
const result = await updateTimeTicket({

View File

@@ -1,6 +1,6 @@
import { useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import day from "../../utils/day";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -34,7 +34,7 @@ export function TimeTicketsCommit({
timeticketIds: timetickets.map((ticket) => ticket.id),
timeticket: {
commited_by: currentUser.email,
committed_at: day()
committed_at: dayjs()
}
},
update(cache) {
@@ -47,7 +47,7 @@ export function TimeTicketsCommit({
return {
...ticket,
commited_by: currentUser.email,
committed_at: day()
committed_at: dayjs()
};
}
return ticket;

View File

@@ -1,7 +1,7 @@
import { useApolloClient } from "@apollo/client";
import { Button, notification } from "antd";
import _ from "lodash";
import day from "../../utils/day";
import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -49,7 +49,7 @@ export function TtApproveButton({
})),
approvalIds: selectedTickets,
approvalUpdate: {
approved_at: day(),
approved_at: dayjs(),
approved_by: currentUser.email
}
}

View File

@@ -1,8 +1,9 @@
import { useEffect, useState, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import SocketIO from "socket.io-client";
import { auth } from "../../firebase/firebase.utils";
import { store } from "../../redux/store";
import { setWssStatus } from "../../redux/application/application.actions";
import { addAlerts, setWssStatus } from "../../redux/application/application.actions";
const useSocket = (bodyshop) => {
const socketRef = useRef(null);
const [clientId, setClientId] = useState(null);
@@ -31,6 +32,14 @@ const useSocket = (bodyshop) => {
socketRef.current = socketInstance;
const handleBodyshopMessage = (message) => {
if (!message || !message?.type) return;
switch (message.type) {
case "alert-update":
store.dispatch(addAlerts(message.payload));
break;
}
if (!import.meta.env.DEV) return;
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
};
@@ -39,22 +48,22 @@ const useSocket = (bodyshop) => {
console.log("Socket connected:", socketInstance.id);
socketInstance.emit("join-bodyshop-room", bodyshop.id);
setClientId(socketInstance.id);
store.dispatch(setWssStatus("connected"))
store.dispatch(setWssStatus("connected"));
};
const handleReconnect = (attempt) => {
console.log(`Socket reconnected after ${attempt} attempts`);
store.dispatch(setWssStatus("connected"))
store.dispatch(setWssStatus("connected"));
};
const handleConnectionError = (err) => {
console.error("Socket connection error:", err);
store.dispatch(setWssStatus("error"))
store.dispatch(setWssStatus("error"));
};
const handleDisconnect = () => {
console.log("Socket disconnected");
store.dispatch(setWssStatus("disconnected"))
store.dispatch(setWssStatus("disconnected"));
};
socketInstance.on("connect", handleConnect);

View File

@@ -81,14 +81,14 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
...additionalParams
};
axios.post("/ioevent", {
useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
operationName: eventName,
variables: additionalParams,
dbevent: false,
env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
});
// axios.post("/ioevent", {
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
// operationName: eventName,
// variables: additionalParams,
// dbevent: false,
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
// });
// console.log(
// "%c[Analytics]",
// "background-color: green ;font-weight:bold;",

View File

@@ -692,6 +692,7 @@ export const GET_JOB_BY_PK = gql`
tax_str_rt
tax_sub_rt
tax_tow_rt
tlos_ind
towin
towing_payable
unit_number

View File

@@ -2,7 +2,7 @@ import { gql } from "@apollo/client";
export const QUERY_TICKETS_BY_JOBID = gql`
query QUERY_TICKETS_BY_JOBID($jobid: uuid!) {
timetickets(where: { jobid: { _eq: $jobid } }, order_by: { date: desc_nulls_first }) {
timetickets(where: { jobid: { _eq: $jobid } }) {
actualhrs
cost_center
ciecacode
@@ -26,7 +26,7 @@ export const QUERY_TICKETS_BY_JOBID = gql`
export const QUERY_TIME_TICKETS_IN_RANGE = gql`
query QUERY_TIME_TICKETS_IN_RANGE($start: date!, $end: date!) {
timetickets(where: { date: { _gte: $start, _lte: $end } }, order_by: { date: desc_nulls_first }) {
timetickets(where: { date: { _gte: $start, _lte: $end } }) {
actualhrs
ciecacode
clockoff
@@ -69,7 +69,6 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
) {
timetickets(
where: { date: { _gte: $start, _lte: $end }, employeeid: { _eq: $employeeid } }
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
@@ -101,7 +100,6 @@ export const QUERY_TIME_TICKETS_TECHNICIAN_IN_RANGE = gql`
}
fixedperiod: timetickets(
where: { date: { _gte: $fixedStart, _lte: $fixedEnd }, employeeid: { _eq: $employeeid } }
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
@@ -145,7 +143,6 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
) {
timetickets(
where: { date: { _gte: $start, _lte: $end }, cost_center: { _neq: "timetickets.labels.shift" } }
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
@@ -180,7 +177,6 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
}
fixedperiod: timetickets(
where: { date: { _gte: $fixedStart, _lte: $fixedEnd }, cost_center: { _neq: "timetickets.labels.shift" } }
order_by: { date: desc_nulls_first }
) {
actualhrs
ciecacode
@@ -335,7 +331,6 @@ export const UPDATE_TIME_TICKETS = gql`
export const QUERY_ACTIVE_TIME_TICKETS = gql`
query QUERY_ACTIVE_TIME_TICKETS($employeeId: uuid) {
timetickets(
order_by: { date: desc_nulls_first }
where: {
_and: {
clockoff: { _is_null: true }

View File

@@ -71,7 +71,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
...logs,
{
timestamp: new Date(),
level: "WARNING",
level: "WARN",
message: "Reconnected to CDK Export Service"
}
];
@@ -123,10 +123,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
socket.emit("set-log-level", value);
}}
>
<Select.Option key="TRACE">TRACE</Select.Option>
<Select.Option key="DEBUG">DEBUG</Select.Option>
<Select.Option key="INFO">INFO</Select.Option>
<Select.Option key="WARNING">WARNING</Select.Option>
<Select.Option key="WARN">WARN</Select.Option>
<Select.Option key="ERROR">ERROR</Select.Option>
</Select>
<Button onClick={() => setLogs([])}>Clear Logs</Button>

View File

@@ -90,7 +90,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
...logs,
{
timestamp: new Date(),
level: "WARNING",
level: "warn",
message: "Reconnected to CDK Export Service"
}
];
@@ -173,10 +173,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
socket.emit("set-log-level", value);
}}
>
<Select.Option key="TRACE">TRACE</Select.Option>
<Select.Option key="DEBUG">DEBUG</Select.Option>
<Select.Option key="INFO">INFO</Select.Option>
<Select.Option key="WARNING">WARNING</Select.Option>
<Select.Option key="WARN">WARN</Select.Option>
<Select.Option key="ERROR">ERROR</Select.Option>
</Select>
<Button onClick={() => setLogs([])}>Clear Logs</Button>

View File

@@ -1,4 +1,4 @@
import { FloatButton, Layout, Spin } from "antd";
import { FloatButton, Layout, notification, Spin } from "antd";
// import preval from "preval.macro";
import React, { lazy, Suspense, useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -21,11 +21,12 @@ import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-st
import { requestForToken } from "../../firebase/firebase.utils";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
import UpdateAlert from "../../components/update-alert/update-alert.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import "./manage.page.styles.scss";
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
import { selectAlerts } from "../../redux/application/application.selectors.js";
import { addAlerts } from "../../redux/application/application.actions.js";
const JobsPage = lazy(() => import("../jobs/jobs.page"));
@@ -104,16 +105,80 @@ const { Content, Footer } = Layout;
const mapStateToProps = createStructuredSelector({
conflict: selectInstanceConflict,
bodyshop: selectBodyshop
bodyshop: selectBodyshop,
alerts: selectAlerts
});
const mapDispatchToProps = (dispatch) => ({});
const ALERT_FILE_URL = InstanceRenderManager({
imex: "https://images.imex.online/alerts/alerts-imex.json",
rome: "https://images.imex.online/alerts/alerts-rome.json"
});
export function Manage({ conflict, bodyshop }) {
const mapDispatchToProps = (dispatch) => ({
setAlerts: (alerts) => dispatch(addAlerts(alerts))
});
export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
const { t } = useTranslation();
const [chatVisible] = useState(false);
const { socket, clientId } = useContext(SocketContext);
// State to track displayed alerts
const [displayedAlertIds, setDisplayedAlertIds] = useState([]);
// Fetch displayed alerts from localStorage on mount
useEffect(() => {
const displayedAlerts = JSON.parse(localStorage.getItem("displayedAlerts") || "[]");
setDisplayedAlertIds(displayedAlerts);
}, []);
// Fetch alerts from the JSON file and dispatch to Redux store
useEffect(() => {
const fetchAlerts = async () => {
try {
const response = await fetch(ALERT_FILE_URL);
const fetchedAlerts = await response.json();
setAlerts(fetchedAlerts);
} catch (error) {
console.error("Error fetching alerts:", error);
}
};
fetchAlerts();
}, []);
// Use useEffect to watch for new alerts
useEffect(() => {
if (alerts && Object.keys(alerts).length > 0) {
// Convert the alerts object into an array
const alertArray = Object.values(alerts);
// Filter out alerts that have already been dismissed
const newAlerts = alertArray.filter((alert) => !displayedAlertIds.includes(alert.id));
newAlerts.forEach((alert) => {
// Display the notification
notification.open({
key: "notification-alerts-" + alert.id,
message: alert.message,
description: alert.description,
type: alert.type || "info",
duration: 0,
placement: "bottomRight",
closable: true,
onClose: () => {
// When the notification is closed, update displayed alerts state and localStorage
setDisplayedAlertIds((prevIds) => {
const updatedIds = [...prevIds, alert.id];
localStorage.setItem("displayedAlerts", JSON.stringify(updatedIds));
return updatedIds;
});
}
});
});
}
}, [alerts, displayedAlertIds]);
useEffect(() => {
const widgetId = InstanceRenderManager({
imex: "IABVNO4scRKY11XBQkNr",

View File

@@ -67,6 +67,12 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
payload: isUpdateAvailable
});
export const addAlerts = (alerts) => ({
type: ApplicationActionTypes.ADD_ALERTS,
payload: alerts
});
export const setWssStatus = (status) => ({
type: ApplicationActionTypes.SET_WSS_STATUS,
payload: status

View File

@@ -15,7 +15,8 @@ const INITIAL_STATE = {
error: null
},
jobReadOnly: false,
partnerVersion: null
partnerVersion: null,
alerts: {}
};
const applicationReducer = (state = INITIAL_STATE, action) => {
@@ -91,6 +92,18 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
case ApplicationActionTypes.SET_WSS_STATUS: {
return { ...state, wssStatus: action.payload };
}
case ApplicationActionTypes.ADD_ALERTS: {
const newAlertsMap = { ...state.alerts };
action.payload.alerts.forEach((alert) => {
newAlertsMap[alert.id] = alert;
});
return {
...state,
alerts: newAlertsMap
};
}
default:
return state;
}

View File

@@ -23,3 +23,4 @@ export const selectOnline = createSelector([selectApplication], (application) =>
export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs);
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
export const selectWssStatus = createSelector([selectApplication], (application) => application.wssStatus);
export const selectAlerts = createSelector([selectApplication], (application) => application.alerts);

View File

@@ -13,6 +13,7 @@ const ApplicationActionTypes = {
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
SET_WSS_STATUS: "SET_WSS_STATUS"
SET_WSS_STATUS: "SET_WSS_STATUS",
ADD_ALERTS: "ADD_ALERTS"
};
export default ApplicationActionTypes;

View File

@@ -28,7 +28,7 @@ import {
} from "../../firebase/firebase.utils";
import { QUERY_EULA } from "../../graphql/bodyshop.queries";
import client from "../../utils/GraphQLClient";
import day from "../../utils/day";
import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import {
checkInstanceId,
@@ -96,7 +96,7 @@ export function* isUserAuthenticated() {
const eulaQuery = yield client.query({
query: QUERY_EULA,
variables: {
now: day()
now: dayjs()
}
});
@@ -314,8 +314,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
try {
const userEmail = yield select((state) => state.user.currentUser.email);
try {
console.log("Setting shop timezone.");
day.tz.setDefault(payload.timezone);
dayjs.tz.setDefault(payload.timezone);
} catch (error) {
console.log(error);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import dayjs from "dayjs";
import "dayjs/locale/en";
import dayjsBusinessDays from "dayjs-business-days2";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import updateLocale from "dayjs/plugin/updateLocale";
@@ -64,4 +65,6 @@ dayjs.extend(minMax);
dayjs.extend(isBetween);
dayjs.extend(dayjsBusinessDays);
dayjs.locale("en");
export default dayjs;

View File

@@ -74,7 +74,7 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SERVICES=ses,secretsmanager,cloudwatch,logs
- SERVICES=s3,ses,secretsmanager,cloudwatch,logs
- DEBUG=0
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
@@ -115,7 +115,8 @@ services:
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 ses verify-email-identity --email-address noreply@imex.online --region ca-central-1
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/id_rsa
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
"
# Node App: The Main IMEX API
node-app:
@@ -167,6 +168,28 @@ services:
# volumes:
# - redis-insight-data:/db
# ##Optional Container for SFTP/SSH Server for testing
# ssh-sftp-server:
# image: atmoz/sftp:alpine # Using an image with SFTP support
# container_name: ssh-sftp-server
# hostname: ssh-sftp-server
# networks:
# - redis-cluster-net
# ports:
# - "2222:22" # Expose port 22 for SSH/SFTP (mapped to 2222 on the host)
# volumes:
# - ./certs/id_rsa.pub:/home/user/.ssh/keys/id_rsa.pub:ro # Mount the SSH public key
# - ./upload:/home/user/upload # Mount a local directory for SFTP uploads
# environment:
# - SFTP_USERS=user:password:1000::upload
# command: >
# /bin/sh -c "
# chmod -R 007 /home/user/upload &&
# echo 'Match User user' >> /etc/ssh/sshd_config &&
# sed -i -e 's#ForceCommand internal-sftp#ForceCommand internal-sftp -d /upload#' /etc/ssh/sshd_config &&
# /usr/sbin/sshd -D
# "
networks:
redis-cluster-net:
driver: bridge

View File

@@ -69,7 +69,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
jobline:
job:
@@ -180,7 +179,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -387,7 +385,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -504,7 +501,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bill:
job:
@@ -671,7 +667,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
_and:
- job:
@@ -1285,7 +1280,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
courtesycar:
bodyshop:
@@ -1526,7 +1520,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -1786,7 +1779,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -1920,7 +1912,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
_or:
- job:
@@ -2105,7 +2096,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
employee_team:
bodyshop:
@@ -2268,7 +2258,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
employee:
bodyshop:
@@ -2449,7 +2438,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -2696,7 +2684,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -2808,7 +2795,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
conversation:
bodyshop:
@@ -3123,7 +3109,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
job:
bodyshop:
@@ -4232,7 +4217,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -4248,41 +4232,41 @@
enable_manual: false
update:
columns:
- clm_no
- v_make_desc
- date_next_contact
- status
- employee_csr
- employee_prep
- clm_total
- suspended
- employee_body
- ro_number
- actual_in
- ownr_co_nm
- v_model_yr
- comment
- job_totals
- v_vin
- ownr_fn
- scheduled_completion
- special_coverage_policy
- v_color
- ca_gst_registrant
- scheduled_delivery
- actual_delivery
- actual_completion
- kanbanparent
- est_ct_fn
- alt_transport
- v_model_desc
- clm_no
- v_make_desc
- date_next_contact
- status
- employee_csr
- actual_in
- v_model_yr
- comment
- job_totals
- ownr_fn
- v_color
- ca_gst_registrant
- employee_refinish
- ownr_ph1
- date_last_contacted
- alt_transport
- inproduction
- est_ct_ln
- production_vars
- category
- v_model_desc
- date_invoiced
- est_co_nm
- ownr_ln
@@ -4295,6 +4279,12 @@
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
body:
action: transform
template: |-
{
"data": {{$body?.event?.data?.new}}
}
method: POST
query_params: {}
template_engine: Kriti
@@ -4496,7 +4486,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
conversation:
bodyshop:
@@ -4670,7 +4659,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
job:
bodyshop:
@@ -4805,7 +4793,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -5110,7 +5097,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
parts_order:
job:
@@ -5243,7 +5229,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
job:
bodyshop:
@@ -5419,7 +5404,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
job:
bodyshop:
@@ -5559,7 +5543,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -5670,7 +5653,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
_or:
- parentjob_rel:
@@ -5760,7 +5742,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
job:
bodyshop:
@@ -6045,7 +6026,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -6541,7 +6521,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:
@@ -6698,7 +6677,6 @@
delete_permissions:
- role: user
permission:
backend_only: false
filter:
bodyshop:
associations:

View File

@@ -0,0 +1,3 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE INDEX idx_timetickets_date ON timetickets (date );

View File

@@ -0,0 +1 @@
CREATE INDEX idx_timetickets_date ON timetickets (date );

View File

@@ -0,0 +1,9 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE INDEX idx_jobs_ownr_fn ON jobs USING gin (ownr_fn gin_trgm_ops);
-- CREATE INDEX idx_jobs_ownr_ln ON jobs USING gin (ownr_ln gin_trgm_ops);
-- CREATE INDEX idx_jobs_ownr_co_nm ON jobs USING gin (ownr_co_nm gin_trgm_ops);
-- CREATE INDEX idx_jobs_clm_no ON jobs USING gin (clm_no gin_trgm_ops);
-- CREATE INDEX idx_jobs_v_make_desc ON jobs USING gin (v_make_desc gin_trgm_ops);
-- CREATE INDEX idx_jobs_v_model_desc ON jobs USING gin (v_model_desc gin_trgm_ops);
-- CREATE INDEX idx_jobs_plate_no ON jobs USING gin (plate_no gin_trgm_ops);

View File

@@ -0,0 +1,7 @@
CREATE INDEX idx_jobs_ownr_fn ON jobs USING gin (ownr_fn gin_trgm_ops);
CREATE INDEX idx_jobs_ownr_ln ON jobs USING gin (ownr_ln gin_trgm_ops);
CREATE INDEX idx_jobs_ownr_co_nm ON jobs USING gin (ownr_co_nm gin_trgm_ops);
CREATE INDEX idx_jobs_clm_no ON jobs USING gin (clm_no gin_trgm_ops);
CREATE INDEX idx_jobs_v_make_desc ON jobs USING gin (v_make_desc gin_trgm_ops);
CREATE INDEX idx_jobs_v_model_desc ON jobs USING gin (v_model_desc gin_trgm_ops);
CREATE INDEX idx_jobs_plate_no ON jobs USING gin (plate_no gin_trgm_ops);

View File

@@ -0,0 +1,3 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE INDEX idx_exportlog_createdat_desc ON exportlog (created_at desc);

View File

@@ -0,0 +1 @@
CREATE INDEX idx_exportlog_createdat_desc ON exportlog (created_at desc);

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE index idx_messages_unread_agg ON messages (read, isoutbound)
-- WHERE read = false AND isoutbound = false;

View File

@@ -0,0 +1,2 @@
CREATE index idx_messages_unread_agg ON messages (read, isoutbound)
WHERE read = false AND isoutbound = false;

View File

@@ -0,0 +1,10 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE INDEX jobs_search_gin_ro_number ON jobs USING GIN ((ro_number) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_ownrfn ON jobs USING GIN ((ownr_fn) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_clm_no ON jobs USING GIN ((clm_no) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_plate_no ON jobs USING GIN ((plate_no) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_v_make_desc ON jobs USING GIN (( v_make_desc) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_v_model_desc ON jobs USING GIN (( v_model_desc) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_ownr_ln ON jobs USING GIN (( ownr_ln) gin_trgm_ops);
-- CREATE INDEX jobs_search_gin_ownr_co_nm ON jobs USING GIN (( ownr_co_nm) gin_trgm_ops);

View File

@@ -0,0 +1,8 @@
CREATE INDEX jobs_search_gin_ro_number ON jobs USING GIN ((ro_number) gin_trgm_ops);
CREATE INDEX jobs_search_gin_ownrfn ON jobs USING GIN ((ownr_fn) gin_trgm_ops);
CREATE INDEX jobs_search_gin_clm_no ON jobs USING GIN ((clm_no) gin_trgm_ops);
CREATE INDEX jobs_search_gin_plate_no ON jobs USING GIN ((plate_no) gin_trgm_ops);
CREATE INDEX jobs_search_gin_v_make_desc ON jobs USING GIN (( v_make_desc) gin_trgm_ops);
CREATE INDEX jobs_search_gin_v_model_desc ON jobs USING GIN (( v_model_desc) gin_trgm_ops);
CREATE INDEX jobs_search_gin_ownr_ln ON jobs USING GIN (( ownr_ln) gin_trgm_ops);
CREATE INDEX jobs_search_gin_ownr_co_nm ON jobs USING GIN (( ownr_co_nm) gin_trgm_ops);

1286
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.679.0",
"@aws-sdk/client-elasticache": "^3.675.0",
"@aws-sdk/client-s3": "^3.689.0",
"@aws-sdk/client-secrets-manager": "^3.675.0",
"@aws-sdk/client-ses": "^3.675.0",
"@aws-sdk/credential-provider-node": "^3.675.0",

View File

@@ -21,7 +21,7 @@ const { applyRedisHelpers } = require("./server/utils/redisHelpers");
const { applyIOHelpers } = require("./server/utils/ioHelpers");
const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents");
const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/client-elasticache");
const { default: InstanceManager } = require("./server/utils/instanceMgr");
const { InstanceRegion } = require("./server/utils/instanceMgr");
const CLUSTER_RETRY_BASE_DELAY = 100;
const CLUSTER_RETRY_MAX_DELAY = 5000;
@@ -114,10 +114,7 @@ const applyRoutes = ({ app }) => {
*/
const getRedisNodesFromAWS = async () => {
const client = new ElastiCacheClient({
region: InstanceManager({
imex: "ca-central-1",
rome: "us-east-2"
})
region: InstanceRegion()
});
const params = {
@@ -153,18 +150,13 @@ const connectToRedisCluster = async () => {
} else {
// Use the Dockerized Redis cluster in development
if (isEmpty(process.env?.REDIS_URL) || !isString(process.env?.REDIS_URL)) {
logger.log(`[${process.env.NODE_ENV}] No or Malformed REDIS_URL present.`, "ERROR", "redis", "api");
logger.log(`No or Malformed REDIS_URL present.`, "ERROR", "redis", "api");
process.exit(1);
}
try {
redisServers = JSON.parse(process.env.REDIS_URL);
} catch (error) {
logger.log(
`[${process.env.NODE_ENV}] Failed to parse REDIS_URL: ${error.message}. Exiting...`,
"ERROR",
"redis",
"api"
);
logger.log(`Failed to parse REDIS_URL: ${error.message}. Exiting...`, "ERROR", "redis", "api");
process.exit(1);
}
}
@@ -172,12 +164,7 @@ const connectToRedisCluster = async () => {
const clusterRetryStrategy = (times) => {
const delay =
Math.min(CLUSTER_RETRY_BASE_DELAY + times * 50, CLUSTER_RETRY_MAX_DELAY) + Math.random() * CLUSTER_RETRY_JITTER;
logger.log(
`[${process.env.NODE_ENV}] Redis cluster not yet ready. Retrying in ${delay.toFixed(2)}ms`,
"WARN",
"redis",
"api"
);
logger.log(`Redis cluster not yet ready. Retrying in ${delay.toFixed(2)}ms`, "WARN", "redis", "api");
return delay;
};
@@ -194,12 +181,12 @@ const connectToRedisCluster = async () => {
return new Promise((resolve, reject) => {
redisCluster.on("ready", () => {
logger.log(`[${process.env.NODE_ENV}] Redis cluster connection established.`, "INFO", "redis", "api");
logger.log(`Redis cluster connection established.`, "INFO", "redis", "api");
resolve(redisCluster);
});
redisCluster.on("error", (err) => {
logger.log(`[${process.env.NODE_ENV}] Redis cluster connection failed: ${err.message}`, "ERROR", "redis", "api");
logger.log(`Redis cluster connection failed: ${err.message}`, "ERROR", "redis", "api");
reject(err);
});
});
@@ -215,7 +202,7 @@ const applySocketIO = async ({ server, app }) => {
// Handle errors
redisCluster.on("error", (err) => {
logger.log(`[${process.env.NODE_ENV}] Redis ERROR`, "ERROR", "redis", "api");
logger.log(`Redis ERROR`, "ERROR", "redis", "api");
});
const pubClient = redisCluster;
@@ -249,7 +236,7 @@ const applySocketIO = async ({ server, app }) => {
});
if (isString(process.env.REDIS_ADMIN_PASS) && !isEmpty(process.env.REDIS_ADMIN_PASS)) {
logger.log(`[${process.env.NODE_ENV}] Initializing Redis Admin UI....`, "INFO", "redis", "api");
logger.log(`Initializing Redis Admin UI....`, "INFO", "redis", "api");
instrument(ioRedis, {
auth: {
type: "basic",
@@ -312,9 +299,9 @@ const main = async () => {
try {
await server.listen(port);
logger.log(`[${process.env.NODE_ENV}] Server started on port ${port}`, "INFO", "api");
logger.log(`Server started on port ${port}`, "INFO", "api");
} catch (error) {
logger.log(`[${process.env.NODE_ENV}] Server failed to start on port ${port}`, "ERROR", "api", error);
logger.log(`Server failed to start on port ${port}`, "ERROR", "api", error);
}
};

View File

@@ -26,7 +26,7 @@ axios.interceptors.request.use((x) => {
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
//console.log(printable);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
return x;
});
@@ -36,7 +36,7 @@ axios.interceptors.response.use((x) => {
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
//console.log(printable);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
return x;
});
@@ -181,7 +181,7 @@ async function QueryBillData(socket, billids) {
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.GET_PBS_AP_ALLOCATIONS, { billids: billids });
CdkBase.createLogEvent(socket, "TRACE", `Bill data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(socket, "SILLY", `Bill data query result ${JSON.stringify(result, null, 2)}`);
return result;
}

View File

@@ -28,7 +28,7 @@ axios.interceptors.request.use((x) => {
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
//console.log(printable);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
return x;
});
@@ -38,7 +38,7 @@ axios.interceptors.response.use((x) => {
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
//console.log(printable);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
return x;
});
@@ -118,7 +118,7 @@ async function CheckForErrors(socket, response) {
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
} else {
CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
CdkBase.createLogEvent(socket, "TRACE", `Error received from DMS: ${JSON.stringify(response)}`);
CdkBase.createLogEvent(socket, "SILLY", `Error received from DMS: ${JSON.stringify(response)}`);
}
}
@@ -130,7 +130,7 @@ async function QueryJobData(socket, jobid) {
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.QUERY_JOBS_FOR_PBS_EXPORT, { id: jobid });
CdkBase.createLogEvent(socket, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
@@ -611,7 +611,7 @@ async function InsertFailedExportLog(socket, error) {
bodyshopid: socket.JobData.bodyshop.id,
jobid: socket.JobData.id,
successful: false,
message: [error],
message: JSON.stringify(error),
useremail: socket.user.email
}
});

View File

@@ -63,7 +63,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
);
if (!account) {
logger.log("qbxml-receivables-no-account", "warn", null, jobline.id, null);
logger.log("qbxml-receivables-no-account", "warn", null, jobline.id);
throw new Error(
`A matching account does not exist for the part allocation. Center: ${jobline.profitcenter_part}`
);

View File

@@ -45,7 +45,7 @@ exports.default = async (req, res) => {
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("qbo-payable-create", "DEBUG", req.user.email, billsToQuery);
logger.log("qbo-payable-create", "DEBUG", req.user.email, null, { billsToQuery });
const result = await client
.setHeaders({ Authorization: BearerToken })
@@ -91,6 +91,12 @@ exports.default = async (req, res) => {
ret.push({ billid: bill.id, success: true });
} catch (error) {
logger.log("qbo-paybles-create-error", "ERROR", req.user.email, null, {
error:
(error && error.authResponse && error.authResponse.body) ||
error.response?.data?.Fault?.Error.map((e) => e.Detail).join(", ") ||
(error && error.message)
});
ret.push({
billid: bill.id,
success: false,
@@ -122,7 +128,7 @@ exports.default = async (req, res) => {
res.status(200).json(ret);
} catch (error) {
//console.log(error);
logger.log("qbo-payable-create-error", "ERROR", req.user.email, {
logger.log("qbo-payable-create-error", "ERROR", req.user.email, null, {
error: error.message,
stack: error.stack
});
@@ -161,7 +167,7 @@ async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) {
async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
const Vendor = {
DisplayName: bill.vendor.name
DisplayName: StandardizeName(bill.vendor.name)
};
try {
const result = await oauthClient.makeApiCall({

View File

@@ -49,7 +49,7 @@ exports.default = async (req, res) => {
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("qbo-payment-create", "DEBUG", req.user.email, paymentsToQuery);
logger.log("qbo-payment-create", "DEBUG", req.user.email, null, { paymentsToQuery });
const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_PAYMENTS_FOR_EXPORT, {
payments: paymentsToQuery
@@ -152,7 +152,7 @@ exports.default = async (req, res) => {
ret.push({ paymentid: payment.id, success: true });
} catch (error) {
logger.log("qbo-payment-create-error", "ERROR", req.user.email, {
logger.log("qbo-payment-create-error", "ERROR", req.user.email, null, {
error: (error && error.authResponse && error.authResponse.body) || (error && error.message)
});
//Add the export log error.
@@ -183,7 +183,7 @@ exports.default = async (req, res) => {
res.status(200).json(ret);
} catch (error) {
//console.log(error);
logger.log("qbo-payment-create-error", "ERROR", req.user.email, {
logger.log("qbo-payment-create-error", "ERROR", req.user.email, null, {
error: error.message,
stack: error.stack
});
@@ -219,6 +219,11 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
PaymentMethodRef: {
value: paymentMethods[payment.type]
},
PrivateNote: payment.memo
? payment.memo.length > 4000
? payment.memo.substring(0, 4000).trim()
: payment.memo.trim()
: "",
PaymentRefNum: payment.transactionid,
...(invoices && invoices.length === 1 && invoices[0]
? {

View File

@@ -10,7 +10,7 @@ function urlBuilder(realmId, object, query = null) {
}
function StandardizeName(str) {
return str.replace(new RegExp(/'/g), "\\'");
return str.replace(new RegExp(/'/g), "\\'").trim();
}
exports.urlBuilder = urlBuilder;

View File

@@ -42,9 +42,10 @@ exports.default = async (req, res) => {
//For each invoice.
res.status(200).json(QbXmlToExecute);
} catch (error) {
logger.log("qbxml-payable-error", "ERROR", req.user.email, req.body.billsToQuery, {
error: error.message,
stack: error.stack
logger.log("qbxml-payable-error", "ERROR", req?.user?.email, null, {
billsToQuery: req?.body?.billsToQuery,
error: error?.message,
stack: error?.stack
});
res.status(400).send(JSON.stringify(error));
}

View File

@@ -21,7 +21,9 @@ exports.default = async (req, res) => {
const client = req.userGraphQLClient;
try {
logger.log("qbxml-payments-create", "DEBUG", req.user.email, req.body.paymentsToQuery, null);
logger.log("qbxml-payments-create", "DEBUG", req?.user?.email, null, {
paymentsToQuery: req.body?.paymentsToQuery
});
const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_PAYMENTS_FOR_EXPORT, {
payments: paymentsToQuery
@@ -62,7 +64,8 @@ exports.default = async (req, res) => {
res.status(200).json(QbXmlToExecute);
} catch (error) {
logger.log("qbxml-payments-error", "error", req.user.email, req.body.paymentsToQuery, {
logger.log("qbxml-payments-error", "error", req?.user?.email, null, {
paymentsToQuery: req.body?.paymentsToQuery,
error: error.message,
stack: error.stack
});

View File

@@ -23,7 +23,9 @@ exports.default = async (req, res) => {
const client = req.userGraphQLClient;
try {
logger.log("qbxml-receivables-create", "DEBUG", req.user.email, req.body.jobIds, null);
logger.log("qbxml-receivables-create", "DEBUG", req?.user?.email, null, {
jobIds: req?.body?.jobIds
});
const result = await client
.setHeaders({ Authorization: BearerToken })
@@ -74,7 +76,8 @@ exports.default = async (req, res) => {
res.status(200).json(QbXmlToExecute);
} catch (error) {
logger.log("qbxml-receivables-error", "error", req.user.email, req.body.jobIds, {
logger.log("qbxml-receivables-error", "error", req?.user?.email, null, {
jobIds: req.body?.jobIds,
error: error.message,
stack: error.stack
});

View File

@@ -0,0 +1,76 @@
const axios = require("axios");
const _ = require("lodash");
const { default: InstanceMgr } = require("../utils/instanceMgr"); // For deep object comparison
// Constants
const ALERTS_REDIS_KEY = "alerts_data"; // The key under which we'll store alerts in Redis
const GLOBAL_SOCKET_ID = "global"; // Use 'global' as a socketId to store global data
const ALERT_FILE_URL = InstanceMgr({
imex: "https://images.imex.online/alerts/alerts-imex.json",
rome: "https://images.imex.online/alerts/alerts-rome.json"
});
const alertCheck = async (req, res) => {
// Access Redis helper functions
const { ioRedis, logger } = req;
const { getSessionData, setSessionData } = req.sessionUtils;
try {
// Get the JSON Alert file from the server
const response = await axios.get(ALERT_FILE_URL);
const currentAlerts = response.data;
// Retrieve stored alerts from Redis using a global socketId
const storedAlerts = await getSessionData(GLOBAL_SOCKET_ID, ALERTS_REDIS_KEY);
if (!storedAlerts) {
// Alerts not in Redis, store them
await setSessionData(GLOBAL_SOCKET_ID, ALERTS_REDIS_KEY, currentAlerts);
logger.logger.debug("Alerts added to Redis for the first time.");
// Emit to clients
if (ioRedis) {
ioRedis.emit("bodyshop-message", {
type: "alert-update",
payload: currentAlerts
});
logger.logger.debug("Alerts emitted to clients for the first time.");
} else {
logger.log("Socket.IO instance not found. (1)", "error");
}
return res.status(200).send("Alerts added to Redis and emitted to clients.");
} else {
// Alerts are in Redis, compare them
if (!_.isEqual(currentAlerts, storedAlerts)) {
// Alerts are different, update Redis and emit to clients
await setSessionData(GLOBAL_SOCKET_ID, ALERTS_REDIS_KEY, currentAlerts);
logger.logger.debug("Alerts updated in Redis.");
// Emit the new alerts to all connected clients
if (ioRedis) {
ioRedis.emit("bodyshop-message", {
type: "alert-update",
payload: currentAlerts
});
logger.logger.debug("Alerts emitted to clients after update.");
} else {
logger.log("Socket.IO instance not found. (2)", "error");
}
return res.status(200).send("Alerts updated in Redis and emitted to clients.");
} else {
return res.status(200).send("No changes in alerts.");
}
}
} catch (error) {
logger.log("Error in alertCheck:", "error", null, null, {
error: {
message: error.message,
stack: error.stack
}
});
return res.status(500).send("Internal server error.");
}
};
module.exports = { alertCheck };

View File

@@ -42,7 +42,7 @@ async function QueryJobData(connectionData, token, jobid) {
CdkBase.createLogEvent(connectionData, "DEBUG", `Querying job data for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client.setHeaders({ Authorization: token }).request(queries.GET_CDK_ALLOCATIONS, { id: jobid });
CdkBase.createLogEvent(connectionData, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(connectionData, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
@@ -373,13 +373,19 @@ function calculateAllocations(connectionData, job) {
});
//profile level adjustments for labor and materials
Object.keys(job.job_totals.rates).forEach((key) => {
if (job.job_totals.rates[key] && job.job_totals.rates[key].adjustment && Dinero(job.job_totals.rates[key].adjustment).isZero() === false) {
if (
job.job_totals.rates[key] &&
job.job_totals.rates[key].adjustment &&
Dinero(job.job_totals.rates[key].adjustment).isZero() === false
) {
const accountName = selectedDmsAllocationConfig.profits[key.toUpperCase()];
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
if (otherAccount) {
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates[key].adjustments));
profitCenterHash[accountName] = profitCenterHash[accountName].add(
Dinero(job.job_totals.rates[key].adjustments)
);
} else {
CdkBase.createLogEvent(
connectionData,

View File

@@ -39,8 +39,8 @@ exports.default = async function ReloadCdkMakes(req, res) {
const deleteResult = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.DELETE_ALL_DMS_VEHICLES, {});
console.log("🚀 ~ file: cdk-get-makes.js ~ line 53 ~ deleteResult", deleteResult);
// logger.logger.debug("🚀 ~ file: cdk-get-makes.js ~ line 53 ~ deleteResult", { deleteResult });
//Insert the new ones.
const insertResult = await client.setHeaders({ Authorization: BearerToken }).request(queries.INSERT_DMS_VEHICLES, {
@@ -59,6 +59,7 @@ exports.default = async function ReloadCdkMakes(req, res) {
cdk_dealerid,
count: newList.length
});
res.sendStatus(200);
} catch (error) {
logger.log("cdk-replace-makes-models-error", "ERROR", req.user.email, null, {

View File

@@ -151,7 +151,7 @@ async function QueryJobData(socket, jobid) {
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.QUERY_JOBS_FOR_CDK_EXPORT, { id: jobid });
CdkBase.createLogEvent(socket, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
@@ -171,7 +171,7 @@ async function CalculateDmsVid(socket, JobData) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientVehicleInsertUpdate.getVehIdsAsync Result ${JSON.stringify(result, null, 2)}`
);
CheckCdkResponseForError(socket, soapResponseVehicleInsertUpdate);
@@ -214,7 +214,7 @@ async function QueryDmsVehicleById(socket, JobData, DMSVid) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientVehicleInsertUpdate.readAsync Result ${JSON.stringify(result, null, 2)}`
);
CdkBase.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.readAsync response.`);
@@ -246,7 +246,7 @@ async function QueryDmsCustomerById(socket, JobData, CustomerId) {
CdkBase.createXmlEvent(socket, rawResponse, `soapClientCustomerInsertUpdate.readAsync response.`);
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientCustomerInsertUpdate.readAsync Result ${JSON.stringify(result, null, 2)}`
);
CheckCdkResponseForError(socket, soapResponseCustomerInsertUpdate);
@@ -295,7 +295,7 @@ async function QueryDmsCustomerByName(socket, JobData) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientCustomerSearch.executeSearchBulkAsync Result ${JSON.stringify(result, null, 2)}`
);
CheckCdkResponseForError(socket, soapResponseCustomerSearch);
@@ -337,7 +337,7 @@ async function GenerateDmsCustomerNumber(socket) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientCustomerInsertUpdate.getCustomerNumberAsync Result ${JSON.stringify(result, null, 2)}`
);
CheckCdkResponseForError(socket, soapResponseCustomerInsertUpdate);
@@ -425,7 +425,7 @@ async function InsertDmsCustomer(socket, newCustomerNumber) {
CdkBase.createXmlEvent(socket, rawResponse, `soapClientCustomerInsertUpdate.insertAsync response.`);
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientCustomerInsertUpdate.insertAsync Result ${JSON.stringify(result, null, 2)}`
);
CheckCdkResponseForError(socket, soapResponseCustomerInsertUpdate);
@@ -505,7 +505,7 @@ async function InsertDmsVehicle(socket) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientVehicleInsertUpdate.insertAsync Result ${JSON.stringify(result, null, 2)}`
);
CdkBase.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.insertAsync response.`);
@@ -611,7 +611,7 @@ async function UpdateDmsVehicle(socket) {
CdkBase.createLogEvent(
socket,
"TRACE",
"DEBUG",
`soapClientVehicleInsertUpdate.updateAsync Result ${JSON.stringify(result, null, 2)}`
);
CdkBase.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.updateAsync response.`);
@@ -650,7 +650,7 @@ async function InsertServiceVehicleHistory(socket) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientServiceHistoryInsert.serviceHistoryHeaderInsert Result ${JSON.stringify(result, null, 2)}`
);
CdkBase.createXmlEvent(socket, rawResponse, `soapClientServiceHistoryInsert.serviceHistoryHeaderInsert response.`);
@@ -690,7 +690,7 @@ async function InsertDmsStartWip(socket) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientAccountingGLInsertUpdate.doStartWIPAsync Result ${JSON.stringify(result, null, 2)}`
);
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doStartWIPAsync response.`);
@@ -721,7 +721,7 @@ async function InsertDmsBatchWip(socket) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientAccountingGLInsertUpdate.doTransBatchWIPAsync Result ${JSON.stringify(result, null, 2)}`
);
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doTransBatchWIPAsync response.`);
@@ -885,7 +885,7 @@ async function PostDmsBatchWip(socket) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync Result ${JSON.stringify(result, null, 2)}`
);
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync response.`);
@@ -914,7 +914,7 @@ async function QueryDmsErrWip(socket) {
CdkBase.createLogEvent(
socket,
"TRACE",
"DEBUG",
`soapClientAccountingGLInsertUpdate.doErrWIPAsync Result ${JSON.stringify(result, null, 2)}`
);
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doErrWIPAsync response.`);
@@ -945,7 +945,7 @@ async function DeleteDmsWip(socket) {
CdkBase.createLogEvent(
socket,
"TRACE",
"SILLY",
`soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync Result ${JSON.stringify(result, null, 2)}`
);
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync response.`);
@@ -995,7 +995,7 @@ async function InsertFailedExportLog(socket, error) {
bodyshopid: socket.JobData.bodyshop.id,
jobid: socket.JobData.id,
successful: false,
message: [error],
message: JSON.stringify(error),
useremail: socket.user.email
}
});

View File

@@ -20,7 +20,7 @@ function CheckCdkResponseForError(socket, soapResponse) {
//The response was null, this might be ok, it might not.
CdkBase.createLogEvent(
socket,
"WARNING",
"warn",
`Warning detected in CDK Response - it appears to be null. Stack: ${new Error().stack}`
);
return;

View File

@@ -13,6 +13,7 @@ let Client = require("ssh2-sftp-client");
const client = require("../graphql-client/graphql-client").client;
const { sendServerEmail } = require("../email/sendemail");
const AHDineroFormat = "0.00";
const AhDateFormat = "MMDDYYYY";
@@ -26,170 +27,180 @@ const ftpSetup = {
password: process.env.AUTOHOUSE_PASSWORD,
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
algorithms: {
serverHostKey: ["ssh-rsa", "ssh-dss"]
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
}
};
const allxmlsToUpload = [];
const allErrors = [];
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("autohouse-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS);
const specificShopIds = req.body.bodyshopIds; // ['uuid]
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
res.sendStatus(401);
return;
}
const allxmlsToUpload = [];
const allErrors = [];
// Send immediate response and continue processing.
res.status(202).json({
success: true,
message: "Processing request ...",
timestamp: new Date().toISOString()
});
try {
for (const bodyshop of specificShopIds ? bodyshops.filter((b) => specificShopIds.includes(b.id)) : bodyshops) {
logger.log("autohouse-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); //Query for the List of Bodyshop Clients.
const specificShopIds = req.body.bodyshopIds; // ['uuid];
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const batchSize = 10;
const shopsToProcess =
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
logger.log("autohouse-shopsToProcess-generated", "DEBUG", "api", null, null);
if (shopsToProcess.length === 0) {
logger.log("autohouse-shopsToProcess-empty", "DEBUG", "api", null, null);
return;
}
const batchPromises = [];
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
const batch = shopsToProcess.slice(i, i + batchSize);
const batchPromise = (async () => {
await processBatch(batch, start, end);
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
} else {
await uploadViaSFTP(allxmlsToUpload);
}
})();
batchPromises.push(batchPromise);
}
await Promise.all(batchPromises);
await sendServerEmail({
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
null,
2
)}`
});
logger.log("autohouse-end", "DEBUG", "api", null, null);
} catch (error) {
logger.log("autohouse-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
}
};
async function processBatch(batch, start, end) {
for (const bodyshop of batch) {
const erroredJobs = [];
try {
logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
const erroredJobs = [];
try {
const { jobs, bodyshops_by_pk } = await client.request(queries.AUTOHOUSE_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
const autoHouseObject = {
AutoHouseExport: {
RepairOrder: jobs.map((j) =>
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
})
)
}
};
if (erroredJobs.length > 0) {
logger.log("autohouse-failed-jobs", "ERROR", "api", bodyshop.id, {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
});
}
var ret = builder
.create(
{
// version: "1.0",
// encoding: "UTF-8",
//keepNullNodes: true,
},
autoHouseObject
)
.end({ allowEmptyTags: true });
allxmlsToUpload.push({
count: autoHouseObject.AutoHouseExport.RepairOrder.length,
xml: ret,
filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml`
});
logger.log("autohouse-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
//Error at the shop level.
logger.log("autohouse-error-shop", "ERROR", "api", bodyshop.id, {
...error
});
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
autuhouseid: bodyshop.autuhouseid,
fatal: true,
errors: [error.toString()]
});
} finally {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
autohouseid: bodyshop.autohouseid,
errors: erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error
}))
});
}
}
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
res.json(allxmlsToUpload);
sendServerEmail({
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`
const { jobs, bodyshops_by_pk } = await client.request(queries.AUTOHOUSE_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
return;
}
let sftp = new Client();
sftp.on("error", (errors) =>
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
...errors
})
);
try {
//Connect to the FTP and upload all.
const autoHouseObject = {
AutoHouseExport: {
RepairOrder: jobs.map((j) =>
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
})
)
}
};
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
logger.log("autohouse-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename
});
const uploadResult = await sftp.put(Buffer.from(xmlObj.xml), `/${xmlObj.filename}`);
logger.log("autohouse-sftp-upload-result", "DEBUG", "api", null, {
uploadResult
if (erroredJobs.length > 0) {
logger.log("autohouse-failed-jobs", "ERROR", "api", bodyshop.id, {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
});
}
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
const ret = builder.create({}, autoHouseObject).end({ allowEmptyTags: true });
allxmlsToUpload.push({
count: autoHouseObject.AutoHouseExport.RepairOrder.length,
xml: ret,
filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml`
});
logger.log("autohouse-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
...error
//Error at the shop level.
logger.log("autohouse-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
autohouseid: bodyshop.autohouseid,
fatal: true,
errors: [error.toString()]
});
} finally {
sftp.end();
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
autohouseid: bodyshop.autohouseid,
errors: erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error
}))
});
}
sendServerEmail({
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`
});
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
}
};
}
async function uploadViaSFTP(allxmlsToUpload) {
const sftp = new Client();
sftp.on("error", (errors) =>
logger.log("autohouse-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("autohouse-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("autohouse-sftp-upload-error", "ERROR", "api", null, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
}
}
} catch (error) {
logger.log("autohouse-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
throw error;
} finally {
sftp.end();
}
}
const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2
@@ -287,8 +298,8 @@ const CreateRepairOrderTag = (job, errorCallback) => {
InsuranceCo: job.ins_co_nm || "",
CompanyName: job.ins_co_nm || "",
Address: job.ins_addr1 || "",
City: job.ins_addr1 || "",
State: job.ins_city || "",
City: job.ins_city || "",
State: job.ins_st || "",
Zip: job.ins_zip || "",
Phone: job.ins_ph1 || "",
Fax: job.ins_fax || "",
@@ -601,10 +612,7 @@ const CreateRepairOrderTag = (job, errorCallback) => {
};
return ret;
} catch (error) {
logger.log("autohouse-job-calculate-error", "ERROR", "api", null, {
error
});
logger.log("autohouse-job-calculate-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
errorCallback({ jobid: job.id, ro_number: job.ro_number, error });
}
};

View File

@@ -22,135 +22,133 @@ const ftpSetup = {
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
}
};
const allcsvsToUpload = [];
const allErrors = [];
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
res.sendStatus(401);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("chatter-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS);
const specificShopIds = req.body.bodyshopIds; // ['uuid]
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const allcsvsToUpload = [];
const allErrors = [];
// Send immediate response and continue processing.
res.status(202).json({
success: true,
message: "Processing request ...",
timestamp: new Date().toISOString()
});
try {
for (const bodyshop of specificShopIds ? bodyshops.filter((b) => specificShopIds.includes(b.id)) : bodyshops) {
logger.log("chatter-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
try {
const { jobs, bodyshops_by_pk } = await client.request(queries.CHATTER_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(1, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
logger.log("chatter-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); //Query for the List of Bodyshop Clients.
const specificShopIds = req.body.bodyshopIds; // ['uuid];
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const chatterObject = jobs.map((j) => {
return {
poc_trigger_code: bodyshops_by_pk.chatterid,
firstname: j.ownr_co_nm ? null : j.ownr_fn,
lastname: j.ownr_co_nm ? j.ownr_co_nm : j.ownr_ln,
transaction_id: j.ro_number,
email: j.ownr_ea,
phone_number: j.ownr_ph1
};
});
const batchSize = 10;
const ret = converter.json2csv(chatterObject, { emptyFieldValue: "" });
const shopsToProcess =
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
logger.log("chatter-shopsToProcess-generated", "DEBUG", "api", null, null);
allcsvsToUpload.push({
count: chatterObject.length,
csv: ret,
filename: `${bodyshop.shopname}_solicitation_${moment().format("YYYYMMDD")}.csv`
});
logger.log("chatter-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
//Error at the shop level.
logger.log("chatter-error-shop", "ERROR", "api", bodyshop.id, {
...error
});
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
shopname: bodyshop.shopname,
fatal: true,
errors: [error.toString()]
});
} finally {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
shopname: bodyshop.shopname
});
}
}
if (skipUpload) {
for (const csvObj of allcsvsToUpload) {
fs.writeFile(`./logs/${csvObj.filename}`, csvObj.csv);
}
sendServerEmail({
subject: `Chatter Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`
});
res.json(allcsvsToUpload);
if (shopsToProcess.length === 0) {
logger.log("chatter-shopsToProcess-empty", "DEBUG", "api", null, null);
return;
}
const sftp = new Client();
sftp.on("error", (errors) => logger.log("chatter-sftp-error", "ERROR", "api", null, { ...errors }));
try {
//Get the private key from AWS Secrets Manager.
ftpSetup.privateKey = await getPrivateKey();
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
for (const csvObj of allcsvsToUpload) {
logger.log("chatter-sftp-upload", "DEBUG", "api", null, { filename: csvObj.filename });
const uploadResult = await sftp.put(Buffer.from(csvObj.xml), `/${csvObj.filename}`);
logger.log("chatter-sftp-upload-result", "DEBUG", "api", null, { uploadResult });
}
} catch (error) {
logger.log("chatter-sftp-error", "ERROR", "api", null, { ...error });
} finally {
sftp.end();
const batchPromises = [];
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
const batch = shopsToProcess.slice(i, i + batchSize);
const batchPromise = (async () => {
await processBatch(batch, start, end);
if (skipUpload) {
for (const csvObj of allcsvsToUpload) {
await fs.promises.writeFile(`./logs/${csvObj.filename}`, csvObj.csv);
}
} else {
await uploadViaSFTP(allcsvsToUpload);
}
})();
batchPromises.push(batchPromise);
}
sendServerEmail({
await Promise.all(batchPromises);
await sendServerEmail({
subject: `Chatter Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
null,
2
)}`
});
res.sendStatus(200);
logger.log("chatter-end", "DEBUG", "api", null, null);
} catch (error) {
res.status(200).json(error);
logger.log("chatter-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
}
};
async function processBatch(batch, start, end) {
for (const bodyshop of batch) {
try {
logger.log("chatter-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
const { jobs, bodyshops_by_pk } = await client.request(queries.CHATTER_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(1, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
const chatterObject = jobs.map((j) => {
return {
poc_trigger_code: bodyshops_by_pk.chatterid,
firstname: j.ownr_co_nm ? null : j.ownr_fn,
lastname: j.ownr_co_nm ? j.ownr_co_nm : j.ownr_ln,
transaction_id: j.ro_number,
email: j.ownr_ea,
phone_number: j.ownr_ph1
};
});
const ret = converter.json2csv(chatterObject, { emptyFieldValue: "" });
allcsvsToUpload.push({
count: chatterObject.length,
csv: ret,
filename: `${bodyshop.shopname}_solicitation_${moment().format("YYYYMMDD")}.csv`
});
logger.log("chatter-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
//Error at the shop level.
logger.log("chatter-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
shopname: bodyshop.shopname,
fatal: true,
errors: [error.toString()]
});
} finally {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
shopname: bodyshop.shopname
});
}
}
}
async function getPrivateKey() {
// Connect to AWS Secrets Manager
const client = new SecretsManagerClient({ region: "ca-central-1" });
@@ -160,9 +158,46 @@ async function getPrivateKey() {
try {
const { SecretString, SecretBinary } = await client.send(command);
if (SecretString || SecretBinary) logger.log("chatter-retrieved-private-key", "DEBUG", "api", null, null);
return SecretString || Buffer.from(SecretBinary, "base64").toString("ascii");
const chatterPrivateKey = SecretString ? SecretString : Buffer.from(SecretBinary, "base64").toString("ascii");
return chatterPrivateKey;
} catch (error) {
logger.log("chatter-get-private-key", "ERROR", "api", null, error);
throw err;
logger.log("chatter-get-private-key", "ERROR", "api", null, { error: error.message, stack: error.stack });
throw error;
}
}
async function uploadViaSFTP(allcsvsToUpload) {
const sftp = new Client();
sftp.on("error", (errors) =>
logger.log("chatter-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
);
try {
//Get the private key from AWS Secrets Manager.
const privateKey = await getPrivateKey();
//Connect to the FTP and upload all.
await sftp.connect({ ...ftpSetup, privateKey });
for (const csvObj of allcsvsToUpload) {
try {
csvObj.result = await sftp.put(Buffer.from(csvObj.csv), `${csvObj.filename}`);
logger.log("chatter-sftp-upload", "DEBUG", "api", null, {
filename: csvObj.filename,
result: csvObj.result
});
} catch (error) {
logger.log("chatter-sftp-upload-error", "ERROR", "api", null, {
filename: csvObj.filename,
error: error.message,
stack: error.stack
});
throw error;
}
}
} catch (error) {
logger.log("chatter-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
throw error;
} finally {
sftp.end();
}
}

View File

@@ -26,174 +26,184 @@ const ftpSetup = {
password: process.env.CLAIMSCORP_PASSWORD,
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
algorithms: {
serverHostKey: ["ssh-rsa", "ssh-dss"]
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
}
};
const allxmlsToUpload = [];
const allErrors = [];
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("claimscorp-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS);
const specificShopIds = req.body.bodyshopIds; // ['uuid]
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
res.sendStatus(401);
return;
}
const allxmlsToUpload = [];
const allErrors = [];
// Send immediate response and continue processing.
res.status(202).json({
success: true,
message: "Processing request ...",
timestamp: new Date().toISOString()
});
try {
for (const bodyshop of specificShopIds ? bodyshops.filter((b) => specificShopIds.includes(b.id)) : bodyshops) {
logger.log("claimscorp-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS); //Query for the List of Bodyshop Clients.
const specificShopIds = req.body.bodyshopIds; // ['uuid];
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const batchSize = 10;
const shopsToProcess =
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
logger.log("claimscorp-shopsToProcess-generated", "DEBUG", "api", null, null);
if (shopsToProcess.length === 0) {
logger.log("claimscorp-shopsToProcess-empty", "DEBUG", "api", null, null);
return;
}
const batchPromises = [];
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
const batch = shopsToProcess.slice(i, i + batchSize);
const batchPromise = (async () => {
await processBatch(batch, start, end);
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
} else {
await uploadViaSFTP(allxmlsToUpload);
}
})();
batchPromises.push(batchPromise);
}
await Promise.all(batchPromises);
await sendServerEmail({
subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`,
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
null,
2
)}`
});
logger.log("claimscorp-end", "DEBUG", "api", null, null);
} catch (error) {
logger.log("claimscorp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
}
};
async function processBatch(batch, start, end) {
for (const bodyshop of batch) {
const erroredJobs = [];
try {
logger.log("claimscorp-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
const erroredJobs = [];
try {
const { jobs, bodyshops_by_pk } = await client.request(queries.CLAIMSCORP_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
const claimsCorpObject = {
DataFeed: {
ShopInfo: {
ShopID: bodyshops_by_pk.claimscorpid,
ShopName: bodyshops_by_pk.shopname,
RO: jobs.map((j) =>
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
})
)
}
}
};
if (erroredJobs.length > 0) {
logger.log("claimscorp-failed-jobs", "ERROR", "api", bodyshop.id, {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
});
}
var ret = builder
.create(
{
// version: "1.0",
// encoding: "UTF-8",
//keepNullNodes: true,
},
claimsCorpObject
)
.end({ allowEmptyTags: true });
allxmlsToUpload.push({
count: claimsCorpObject.DataFeed.ShopInfo.RO.length,
xml: ret,
filename: `${bodyshop.claimscorpid}-${moment().format("YYYYMMDDTHHMMss")}.xml`
});
logger.log("claimscorp-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
//Error at the shop level.
logger.log("claimscorp-error-shop", "ERROR", "api", bodyshop.id, {
...error
});
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
claimscorpid: bodyshop.claimscorpid,
fatal: true,
errors: [error.toString()]
});
} finally {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
claimscorpid: bodyshop.claimscorpid,
errors: erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error
}))
});
}
}
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
res.json(allxmlsToUpload);
sendServerEmail({
subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`
const { jobs, bodyshops_by_pk } = await client.request(queries.CLAIMSCORP_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
return;
}
let sftp = new Client();
sftp.on("error", (errors) =>
logger.log("claimscorp-sftp-error", "ERROR", "api", null, {
...errors
})
);
try {
//Connect to the FTP and upload all.
const claimsCorpObject = {
DataFeed: {
ShopInfo: {
ShopID: bodyshops_by_pk.claimscorpid,
ShopName: bodyshops_by_pk.shopname,
RO: jobs.map((j) =>
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
})
)
}
}
};
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename
});
const uploadResult = await sftp.put(Buffer.from(xmlObj.xml), `/${xmlObj.filename}`);
logger.log("claimscorp-sftp-upload-result", "DEBUG", "api", null, {
uploadResult
if (erroredJobs.length > 0) {
logger.log("claimscorp-failed-jobs", "ERROR", "api", bodyshop.id, {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
});
}
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
const ret = builder.create({}, claimsCorpObject).end({ allowEmptyTags: true });
allxmlsToUpload.push({
count: claimsCorpObject.DataFeed.ShopInfo.RO.length,
xml: ret,
filename: `${bodyshop.claimscorpid}-${moment().format("YYYYMMDDTHHMMss")}.xml`
});
logger.log("claimscorp-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
logger.log("claimscorp-sftp-error", "ERROR", "api", null, {
...error
//Error at the shop level.
logger.log("claimscorp-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
claimscorpid: bodyshop.claimscorpid,
fatal: true,
errors: [error.toString()]
});
} finally {
sftp.end();
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
claimscorpid: bodyshop.claimscorpid,
errors: erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error
}))
});
}
sendServerEmail({
subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`
});
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
}
};
}
async function uploadViaSFTP(allxmlsToUpload) {
const sftp = new Client();
sftp.on("error", (errors) =>
logger.log("claimscorp-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("claimscorp-sftp-upload-error", "ERROR", "api", null, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
}
}
} catch (error) {
logger.log("claimscorp-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
throw error;
} finally {
sftp.end();
}
}
const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2
@@ -445,10 +455,7 @@ const CreateRepairOrderTag = (job, errorCallback) => {
};
return ret;
} catch (error) {
logger.log("claimscorp-job-calculate-error", "ERROR", "api", null, {
error
});
logger.log("claimscorp-job-calculate-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
errorCallback({ jobid: job.id, ro_number: job.ro_number, error });
}
};

View File

@@ -16,8 +16,7 @@ const { sendServerEmail } = require("../email/sendemail");
const DineroFormat = "0,0.00";
const DateFormat = "MM/DD/YYYY";
const repairOpCodes = ["OP4", "OP9", "OP10"];
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];
const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE", "SHAW"];
const ftpSetup = {
host: process.env.KAIZEN_HOST,
@@ -30,173 +29,179 @@ const ftpSetup = {
}
};
const allxmlsToUpload = [];
const allErrors = [];
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("kaizen-start", "DEBUG", "api", null, null);
const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE", "SHAW"];
const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, {
imexshopid: kaizenShopsIDs
});
const specificShopIds = req.body.bodyshopIds; // ['uuid]
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
res.sendStatus(401);
return;
}
const allxmlsToUpload = [];
const allErrors = [];
// Send immediate response and continue processing.
res.status(202).json({
success: true,
message: "Processing request ...",
timestamp: new Date().toISOString()
});
try {
for (const bodyshop of specificShopIds ? bodyshops.filter((b) => specificShopIds.includes(b.id)) : bodyshops) {
logger.log("kaizen-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { imexshopid: kaizenShopsIDs }); //Query for the List of Bodyshop Clients.
const specificShopIds = req.body.bodyshopIds; // ['uuid];
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const batchSize = 10;
const shopsToProcess =
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
logger.log("kaizen-shopsToProcess-generated", "DEBUG", "api", null, null);
if (shopsToProcess.length === 0) {
logger.log("kaizen-shopsToProcess-empty", "DEBUG", "api", null, null);
return;
}
const batchPromises = [];
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
const batch = shopsToProcess.slice(i, i + batchSize);
const batchPromise = (async () => {
await processBatch(batch, start, end);
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
} else {
await uploadViaSFTP(allxmlsToUpload);
}
})();
batchPromises.push(batchPromise);
}
await Promise.all(batchPromises);
await sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
null,
2
)}`
});
logger.log("kaizen-end", "DEBUG", "api", null, null);
} catch (error) {
logger.log("kaizen-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
}
};
async function processBatch(batch, start, end) {
for (const bodyshop of batch) {
const erroredJobs = [];
try {
logger.log("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
const erroredJobs = [];
try {
const { jobs, bodyshops_by_pk } = await client.request(queries.KAIZEN_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
const kaizenObject = {
DataFeed: {
ShopInfo: {
ShopName: bodyshops_by_pk.shopname,
Jobs: jobs.map((j) =>
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
})
)
}
}
};
if (erroredJobs.length > 0) {
logger.log("kaizen-failed-jobs", "ERROR", "api", bodyshop.id, {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
});
}
var ret = builder
.create(
{
// version: "1.0",
// encoding: "UTF-8",
//keepNullNodes: true,
},
kaizenObject
)
.end({ allowEmptyTags: true });
allxmlsToUpload.push({
count: kaizenObject.DataFeed.ShopInfo.Jobs.length,
xml: ret,
filename: `${bodyshop.shopname}-${moment().format("YYYYMMDDTHHMMss")}.xml`
});
logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
//Error at the shop level.
logger.log("kaizen-error-shop", "ERROR", "api", bodyshop.id, {
...error
});
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
shopname: bodyshop.shopname,
fatal: true,
errors: [error.toString()]
});
} finally {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
shopname: bodyshop.shopname,
errors: erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error
}))
});
}
}
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
res.json(allxmlsToUpload);
sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`
const { jobs, bodyshops_by_pk } = await client.request(queries.KAIZEN_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
return;
}
let sftp = new Client();
sftp.on("error", (errors) =>
logger.log("kaizen-sftp-error", "ERROR", "api", null, {
...errors
})
);
try {
//Connect to the FTP and upload all.
const kaizenObject = {
DataFeed: {
ShopInfo: {
ShopName: bodyshops_by_pk.shopname,
Jobs: jobs.map((j) =>
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
})
)
}
}
};
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
logger.log("kaizen-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename
});
const uploadResult = await sftp.put(Buffer.from(xmlObj.xml), `/${xmlObj.filename}`);
logger.log("kaizen-sftp-upload-result", "DEBUG", "api", null, {
uploadResult
if (erroredJobs.length > 0) {
logger.log("kaizen-failed-jobs", "ERROR", "api", bodyshop.id, {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
});
}
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
const ret = builder.create({}, kaizenObject).end({ allowEmptyTags: true });
allxmlsToUpload.push({
count: kaizenObject.DataFeed.ShopInfo.Jobs.length,
xml: ret,
filename: `${bodyshop.shopname}-${moment().format("YYYYMMDDTHHMMss")}.xml`
});
logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
logger.log("kaizen-sftp-error", "ERROR", "api", null, {
...error
//Error at the shop level.
logger.log("kaizen-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
shopname: bodyshop.shopname,
fatal: true,
errors: [error.toString()]
});
} finally {
sftp.end();
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
shopname: bodyshop.shopname,
errors: erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error
}))
});
}
sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`
});
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
}
};
}
async function uploadViaSFTP(allxmlsToUpload) {
const sftp = new Client();
sftp.on("error", (errors) =>
logger.log("kaizen-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("kaizen-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("kaizen-sftp-upload-error", "ERROR", "api", null, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
}
}
} catch (error) {
logger.log("kaizen-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
throw error;
} finally {
sftp.end();
}
}
const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2
@@ -420,10 +425,7 @@ const CreateRepairOrderTag = (job, errorCallback) => {
};
return ret;
} catch (error) {
logger.log("kaizen-job-calculate-error", "ERROR", "api", null, {
error
});
logger.log("kaizen-job-calculate-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
errorCallback({ jobid: job.id, ro_number: job.ro_number, error });
}
};

View File

@@ -1,25 +1,21 @@
const { isString, isEmpty } = require("lodash");
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
const { default: InstanceManager } = require("../utils/instanceMgr");
const { InstanceRegion } = require("../utils/instanceMgr");
const aws = require("@aws-sdk/client-ses");
const nodemailer = require("nodemailer");
const logger = require("../utils/logger");
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
const sesConfig = {
apiVersion: "latest",
credentials: defaultProvider(),
region: isLocal
? "ca-central-1"
: InstanceManager({
imex: "ca-central-1",
rome: "us-east-2"
})
region: InstanceRegion()
};
if (isLocal) {
sesConfig.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
console.log(`SES Mailer set to LocalStack end point: ${sesConfig.endpoint}`);
logger.logger.debug(`SES Mailer set to LocalStack end point: ${sesConfig.endpoint}`);
}
const ses = new aws.SES(sesConfig);

View File

@@ -21,7 +21,7 @@ const getImage = async (imageUrl) => {
// Log the email in the database
const logEmail = async (req, email) => {
try {
const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, {
await client.request(queries.INSERT_EMAIL_AUDIT, {
email: {
to: email.to,
cc: email.cc,
@@ -34,13 +34,13 @@ const logEmail = async (req, email) => {
status: "Sent"
}
});
console.log(insertresult);
} catch (error) {
logger.log("email-log-error", "error", req.user.email, null, {
logger.log("email-log-error", "error", req?.user?.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject
to: req?.body?.to,
cc: req?.body?.cc,
subject: req?.body?.subject,
email
// info,
});
}
@@ -70,12 +70,11 @@ const sendServerEmail = async ({ subject, text }) => {
}
},
(err, info) => {
console.log(err || info);
logger.log("server-email-failure", err ? "error" : "debug", null, null, { message: err || info });
}
);
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
logger.log("server-email-failure", "error", null, null, { error });
}
};
@@ -88,8 +87,7 @@ const sendProManagerWelcomeEmail = async ({ to, subject, html }) => {
html
});
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
logger.log("server-email-failure", "error", null, null, { error });
}
};
@@ -108,12 +106,12 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
attachments: attachments || null
},
(err, info) => {
console.log(err || info);
// (message, type, user, record, meta
logger.log("server-email", err ? "error" : "debug", null, null, { message: err || info });
}
);
} catch (error) {
console.log(error);
logger.log("server-email-failure", "error", null, null, error);
logger.log("server-email-failure", "error", null, null, { error });
}
};
@@ -184,9 +182,8 @@ const sendEmail = async (req, res) => {
}
},
(err, info) => {
console.log(err || info);
if (info) {
logger.log("send-email-success", "DEBUG", req.user.email, null, {
logger.log("send-email-success", "DEBUG", req?.user?.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
@@ -205,7 +202,7 @@ const sendEmail = async (req, res) => {
success: true //response: info
});
} else {
logger.log("send-email-failure", "ERROR", req.user.email, null, {
logger.log("send-email-failure", "ERROR", req?.user?.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
@@ -290,7 +287,9 @@ ${body.bounce?.bouncedRecipients.map(
`
},
(err, info) => {
console.log("***", err || info);
logger.log("sns-error", err ? "error" : "debug", "api", null, {
message: err ? JSON.stringify(error) : info
});
}
);
}

View File

@@ -18,10 +18,10 @@ const tasksEmailQueue = taskEmailQueue();
const tasksEmailQueueCleanup = async () => {
try {
// Example async operation
console.log("Performing Tasks Email Reminder process cleanup...");
// console.log("Performing Tasks Email Reminder process cleanup...");
await new Promise((resolve) => tasksEmailQueue.destroy(() => resolve()));
} catch (err) {
console.error("Tasks Email Reminder process cleanup failed:", err);
// console.error("Tasks Email Reminder process cleanup failed:", err);
}
};

View File

@@ -10,8 +10,9 @@ const logger = require("../utils/logger");
const taskEmailQueue = () =>
new Queue(
(taskIds, cb) => {
console.log("Processing reminds for taskIds: ", taskIds.join(", "));
logger.log("Processing reminds for taskIds: ", "silly", null, null, {
taskIds: taskIds?.join(", ")
});
// Set the remind_at_sent to the current time.
const now = moment().toISOString();

View File

@@ -269,10 +269,14 @@ const sendNotification = async (req, res) => {
})
.then((response) => {
// Response is a message ID string.
console.log("Successfully sent message:", response);
logger.log("Successfully sent message:", "debug", req?.user?.email, null, {
response
});
})
.catch((error) => {
console.log("Error sending message:", error);
logger.log("Successfully sent message:", "error", req?.user?.email, null, {
error
});
});
res.sendStatus(200);

View File

@@ -17,12 +17,10 @@ require("dotenv").config({
const domain = process.env.NODE_ENV ? "secure" : "test";
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
const { InstanceRegion } = require("../utils/instanceMgr");
const client = new SecretsManagerClient({
region: InstanceManager({
imex: "ca-central-1",
rome: "us-east-2"
})
region: InstanceRegion()
});
const gqlClient = require("../graphql-client/graphql-client").client;

View File

@@ -35,7 +35,7 @@ exports.default = async (req, res) => {
res.sendStatus(200);
} catch (error) {
logger.log("ioevent-error", "trace", user, null, {
logger.log("ioevent-error", "silly", user, null, {
operationname: operationName,
time,
dbevent,

View File

@@ -19,7 +19,8 @@ async function JobCosting(req, res) {
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
//Uncomment for further testing
// logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
try {
const resp = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_JOB_COSTING_DETAILS, {
@@ -46,7 +47,10 @@ async function JobCostingMulti(req, res) {
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("job-costing-multi-start", "DEBUG", req.user.email, jobids, null);
//Uncomment for further testing
// logger.log("job-costing-multi-start", "DEBUG", req?.user?.email, null, {
// jobids
// });
try {
const resp = await client
@@ -244,7 +248,8 @@ async function JobCostingMulti(req, res) {
data: ret
});
} catch (error) {
logger.log("job-costing-multi-error", "ERROR", req.user.email, [jobids], {
logger.log("job-costing-multi-error", "ERROR", req?.user?.email, null, {
jobids,
message: error.message,
stack: error.stack
});
@@ -282,7 +287,13 @@ function GenerateCostingData(job) {
if (val.mod_lbr_ty) {
const laborProfitCenter = val.profitcenter_labor || defaultProfits[val.mod_lbr_ty] || "Unknown";
if (laborProfitCenter === "Unknown") console.log("Unknown type", val.line_desc, val.mod_lbr_ty);
//Uncomment for further testing
// if (laborProfitCenter === "Unknown") {
// logger.log("job-costing unknown type", "debug", null, null, {
// line_desc: val.line_desc,
// mod_lbr_ty: val.mod_lbr_ty
// });
// }
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
@@ -349,10 +360,22 @@ function GenerateCostingData(job) {
if (val.part_type && val.part_type !== "PAE" && val.part_type !== "PAS" && val.part_type !== "PASL") {
const partsProfitCenter = val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
if (partsProfitCenter === "Unknown") console.log("Unknown type", val.line_desc, val.part_type);
//Uncomment for further testing
// if (partsProfitCenter === "Unknown" || !partsProfitCenter) {
// logger.log(
// partsProfitCenter === "Unknown"
// ? "job-costing unknown type"
// : "Unknown cost/profit center mapping for parts.",
// "debug",
// null,
// null,
// {
// line_desc: val.line_desc,
// part_type: val.part_type
// }
// );
// }
if (!partsProfitCenter)
console.log("Unknown cost/profit center mapping for parts.", val.line_desc, val.part_type);
let partsAmount = Dinero({
amount: val.act_price_before_ppc
? Math.round(val.act_price_before_ppc * 100)
@@ -409,10 +432,22 @@ function GenerateCostingData(job) {
if (val.part_type && val.part_type !== "PAE" && (val.part_type === "PAS" || val.part_type === "PASL")) {
const partsProfitCenter = val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
if (partsProfitCenter === "Unknown") console.log("Unknown type", val.line_desc, val.part_type);
//Uncomment for further testing
// if (partsProfitCenter === "Unknown" || !partsProfitCenter) {
// logger.log(
// partsProfitCenter === "Unknown"
// ? "job-costing unknown type"
// : "job-costing Unknown cost/profit center mapping for sublet",
// "debug",
// null,
// null,
// {
// line_desc: val.line_desc,
// part_type: val.part_type
// }
// );
// }
if (!partsProfitCenter)
console.log("Unknown cost/profit center mapping for sublet.", val.line_desc, val.part_type);
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100)
})
@@ -443,9 +478,14 @@ function GenerateCostingData(job) {
//If so, use it, otherwise try to use the same from the auto-allocate logic in IO app jobs-close-auto-allocate.
const partsProfitCenter = val.profitcenter_part || getAdditionalCostCenter(val, defaultProfits) || "Unknown";
if (partsProfitCenter === "Unknown") {
console.log("Unknown type", val.line_desc, val.part_type);
}
//Uncomment for further testing
// if (partsProfitCenter === "Unknown") {
// logger.log("job-costing unknown type", "debug", null, null, {
// line_desc: val.line_desc,
// part_type: val.part_type
// });
// }
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100)
})
@@ -476,7 +516,7 @@ function GenerateCostingData(job) {
if (!hasMapaLine) {
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]])
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = Dinero();
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = jobLineTotalsByProfitCenter.additional[
defaultProfits["MAPA"]
].add(

View File

@@ -78,16 +78,20 @@ const jobLifecycle = async (req, res) => {
Object.keys(flatGroupedAllDurations).forEach((status) => {
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
const humanReadable = durationToHumanReadable(moment.duration(value));
const percentage = (value / finalTotal) * 100;
const percentage = finalTotal > 0 ? (value / finalTotal) * 100 : 0;
const color = getLifecycleStatusColor(status);
const roundedPercentage = `${Math.round(percentage)}%`;
const averageValue = _.size(jobIDs) > 0 ? value / jobIDs.length : 0;
const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue));
finalSummations.push({
status,
value,
humanReadable,
percentage,
color,
roundedPercentage
roundedPercentage,
averageValue,
averageHumanReadable
});
});
@@ -100,7 +104,12 @@ const jobLifecycle = async (req, res) => {
totalStatuses: finalSummations.length,
total: finalTotal,
statusCounts: finalStatusCounts,
humanReadable: durationToHumanReadable(moment.duration(finalTotal))
humanReadable: durationToHumanReadable(moment.duration(finalTotal)),
averageValue: _.size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0,
averageHumanReadable:
_.size(jobIDs) > 0
? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length))
: durationToHumanReadable(moment.duration(0))
}
});
};

View File

@@ -26,7 +26,7 @@ exports.totalsSsu = async function (req, res) {
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("job-totals-ssu-USA", "DEBUG", req.user.email, id, null);
logger.log("job-totals-ssu-USA", "DEBUG", req?.user?.email, id);
try {
const job = await client.setHeaders({ Authorization: BearerToken }).request(queries.GET_JOB_BY_PK, {
@@ -47,7 +47,7 @@ exports.totalsSsu = async function (req, res) {
res.status(200).send();
} catch (error) {
logger.log("job-totals-ssu-USA-error", "ERROR", req.user.email, id, {
logger.log("job-totals-ssu-USA-error", "ERROR", req?.user?.email, id, {
jobid: id,
error
});
@@ -84,12 +84,10 @@ async function Totals(req, res) {
const logger = req.logger;
const client = req.userGraphQLClient;
logger.log("job-totals-USA", "DEBUG", req.user.email, job.id, {
logger.log("job-totals-ssu-USA", "DEBUG", req.user.email, job.id, {
jobid: job.id
});
logger.log("job-totals-ssu-USA", "DEBUG", req.user.email, id, null);
await AutoAddAtsIfRequired({ job, client });
try {
@@ -961,7 +959,9 @@ function CalculateTaxesTotals(job, otherTotals) {
}
}
} catch (error) {
console.error("Key with issue", key);
logger.log("job-totals-USA Key with issue", "error", null, null, {
key
});
}
});
@@ -1026,7 +1026,9 @@ function CalculateTaxesTotals(job, otherTotals) {
totalTaxByTier[taxTierKey] = totalTaxByTier[taxTierKey].add(taxAmountToAdd);
}
} catch (error) {
console.error("PFP Calculation error", error);
logger.log("job-totals-USA - PFP Calculation Error", "error", null, null, {
error
});
}
});

View File

@@ -2,8 +2,16 @@ const { isObject } = require("lodash");
const jobUpdated = async (req, res) => {
const { ioRedis, logger, ioHelpers } = req;
// Old Way
if (req?.body?.event?.data?.new || isObject(req?.body?.event?.data?.new)) {
const updatedJob = req.body.event.data.new;
const bodyshopID = updatedJob.shopid;
ioRedis.to(ioHelpers.getBodyshopRoom(bodyshopID)).emit("production-job-updated", updatedJob);
return res.json({ message: "Job updated and event emitted" });
}
if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) {
// New way
if (!req?.body?.data || !isObject(req.body.data)) {
logger.log("job-update-error", "ERROR", req.user?.email, null, {
message: `Malformed Job Update request sent from Hasura`,
body: req?.body
@@ -15,12 +23,14 @@ const jobUpdated = async (req, res) => {
});
}
logger.log("job-update", "INFO", req.user?.email, null, {
message: `Job updated event received from Hasura`,
jobid: req?.body?.event?.data?.new?.id
});
// Uncomment for further testing
// You can also test this using SocketIOAdmin
// logger.log("job-update", "DEBUG", req.user?.email, null, {
// message: `Job updated event received from Hasura`,
// jobid: req?.body?.event?.data?.new?.id
// });
const updatedJob = req.body.event.data.new;
const updatedJob = req.body.data;
const bodyshopID = updatedJob.shopid;
// Emit the job-updated event only to the room corresponding to the bodyshop

Some files were not shown because too many files have changed in this diff Show More