Compare commits
136 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77f340d08c | ||
|
|
d40579694f | ||
|
|
fa24d87966 | ||
|
|
1b6eab8488 | ||
|
|
e202bf9a89 | ||
|
|
1a6e8bc5ba | ||
|
|
dd4ba8a467 | ||
|
|
8ad1dd83c6 | ||
|
|
80b66fd7e8 | ||
|
|
45ac56e0bc | ||
|
|
1ff1de8739 | ||
|
|
ea1cc23ee7 | ||
|
|
7cbabf8697 | ||
|
|
289a666b6d | ||
|
|
b8836c7ae1 | ||
|
|
eca31c5618 | ||
|
|
7fdbedefce | ||
|
|
7140b8d585 | ||
|
|
5eed8d9809 | ||
|
|
57fe5b4c46 | ||
|
|
f266ee1cfe | ||
|
|
9550de5131 | ||
|
|
1f76ff882c | ||
|
|
749f73a272 | ||
|
|
9c1774c417 | ||
|
|
26b3a43ce5 | ||
|
|
78678dd3dc | ||
|
|
9dc4546b2e | ||
|
|
95aa0e45a6 | ||
|
|
ce9a77efcf | ||
|
|
e9e1e820a7 | ||
|
|
b027a4e618 | ||
|
|
c7fc75aa5c | ||
|
|
98d2372daf | ||
|
|
bf51380167 | ||
|
|
1ec827097f | ||
|
|
89fabf85e1 | ||
|
|
ff7dd7d3ea | ||
|
|
8cc4f88fa7 | ||
|
|
2439755f9e | ||
|
|
7e6ab3a5ff | ||
|
|
763384f05f | ||
|
|
34f876f838 | ||
|
|
cba2da8da7 | ||
|
|
f3d8aa3438 | ||
|
|
2f3eccf3d8 | ||
|
|
2b3e64d607 | ||
|
|
05b20505bb | ||
|
|
bddeae945c | ||
|
|
5b267f03b9 | ||
|
|
357d916e0a | ||
|
|
6ed12ebe7d | ||
|
|
6703bc025d | ||
|
|
387dac6779 | ||
|
|
6f454dd4cb | ||
|
|
1440a60228 | ||
|
|
f2aa3960aa | ||
|
|
8d4195b596 | ||
|
|
06508f3ad8 | ||
|
|
9e190e7fb7 | ||
|
|
5cbf00b0c8 | ||
|
|
655aeb86fc | ||
|
|
225549275d | ||
|
|
f0717b8b36 | ||
|
|
78771ae750 | ||
|
|
0389908398 | ||
|
|
54bee763df | ||
|
|
1117a94930 | ||
|
|
5fbfb992c7 | ||
|
|
87b3b65f3e | ||
|
|
9970190909 | ||
|
|
8eee371a90 | ||
|
|
ba97b1efef | ||
|
|
8d8887c28e | ||
|
|
3b19432974 | ||
|
|
a14b2340b0 | ||
|
|
624f8e77cb | ||
|
|
fb624c817d | ||
|
|
c2b4b66ed1 | ||
|
|
ffec03ab6c | ||
|
|
552163d7b9 | ||
|
|
db1f59578c | ||
|
|
8ec5831ec5 | ||
|
|
0146ac5b7b | ||
|
|
a603e5c0b8 | ||
|
|
9aab47d8f8 | ||
|
|
f2f84e2da8 | ||
|
|
338906e288 | ||
|
|
542997b1a7 | ||
|
|
5fce548666 | ||
|
|
80322caad0 | ||
|
|
56472d24d9 | ||
|
|
db5dcc271d | ||
|
|
73ab02225e | ||
|
|
83a1b7690d | ||
|
|
c9e28b1ed2 | ||
|
|
c25c66d00f | ||
|
|
d319ab49d4 | ||
|
|
a069989ea7 | ||
|
|
8e3aa186cb | ||
|
|
01c55d6277 | ||
|
|
3438907d8d | ||
|
|
ae020b651e | ||
|
|
d22988df15 | ||
|
|
8136a56ad2 | ||
|
|
830f6c0eea | ||
|
|
4c1849289a | ||
|
|
c45a4780e3 | ||
|
|
d4adc4c1aa | ||
|
|
d9e71423f5 | ||
|
|
6cac0f9594 | ||
|
|
2ab4615642 | ||
|
|
dd5961d419 | ||
|
|
8190958ba3 | ||
|
|
77e009f316 | ||
|
|
2b2738a8d1 | ||
|
|
3d10c9da7f | ||
|
|
e82c77d119 | ||
|
|
855a78be05 | ||
|
|
a29e840797 | ||
|
|
1b30c1ab58 | ||
|
|
80f235f12e | ||
|
|
9b67148522 | ||
|
|
6b501e4619 | ||
|
|
42f1d6fa13 | ||
|
|
3f247a9227 | ||
|
|
63b914731b | ||
|
|
23f8f69bbe | ||
|
|
fc3ea2bdf8 | ||
|
|
96e970faf7 | ||
|
|
c133195607 | ||
|
|
d75ea2b1a6 | ||
|
|
ec0fd840e4 | ||
|
|
19050d31f7 | ||
|
|
5b00ded5f6 | ||
|
|
c5b19d8f22 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text eol=lf
|
||||
24
.platform/hooks/predeploy/00-install-fonts.sh
Normal file
24
.platform/hooks/predeploy/00-install-fonts.sh
Normal 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."
|
||||
30
.vscode/settings.json
vendored
30
.vscode/settings.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
12
certs/io-ftp-test.key
Normal file
12
certs/io-ftp-test.key
Normal file
@@ -0,0 +1,12 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBYJnAujo17diR0fM2Ze1d1Ft6XHm5
|
||||
U31pXdFEN+rGC4SoYTdZE8q3relxMS5GwwBOvgvVUuayfid2XS8ls/CMDiMBJAYqEK4CRY
|
||||
PbbPB7lLnMWsF7muFhvs+SIpPQC+vtDwM2TKlxF0Y8p+iVRpvCADoggsSze7skmJWKmMTt
|
||||
8jEdEOcAAAEQIyXsOSMl7DkAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
|
||||
AAAIUEAWCZwLo6Ne3YkdHzNmXtXdRbelx5uVN9aV3RRDfqxguEqGE3WRPKt63pcTEuRsMA
|
||||
Tr4L1VLmsn4ndl0vJbPwjA4jASQGKhCuAkWD22zwe5S5zFrBe5rhYb7PkiKT0Avr7Q8DNk
|
||||
ypcRdGPKfolUabwgA6IILEs3u7JJiVipjE7fIxHRDnAAAAQUO5dO9G7i0bxGTP0zV3eIwv
|
||||
5g0NhrQJfW/bMHS6XWwaxdpr+QZ+DbBJVzZPwYC0wLMW4bJAf+kjqUnj4wGocoTeAAAAD2
|
||||
lvLWZ0cC10ZXN0LWtleQECAwQ=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
certs/io-ftp-test.key.pub
Normal file
1
certs/io-ftp-test.key.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFgmcC6OjXt2JHR8zZl7V3UW3pceblTfWld0UQ36sYLhKhhN1kTyret6XExLkbDAE6+C9VS5rJ+J3ZdLyWz8IwOIwEkBioQrgJFg9ts8HuUucxawXua4WG+z5Iik9AL6+0PAzZMqXEXRjyn6JVGm8IAOiCCxLN7uySYlYqYxO3yMR0Q5w== io-ftp-test-key
|
||||
12
certs/io-ftp-test.ppk
Normal file
12
certs/io-ftp-test.ppk
Normal file
@@ -0,0 +1,12 @@
|
||||
PuTTY-User-Key-File-3: ecdsa-sha2-nistp521
|
||||
Encryption: none
|
||||
Comment: io-ftp-test-key
|
||||
Public-Lines: 4
|
||||
AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFgmcC6OjXt
|
||||
2JHR8zZl7V3UW3pceblTfWld0UQ36sYLhKhhN1kTyret6XExLkbDAE6+C9VS5rJ+
|
||||
J3ZdLyWz8IwOIwEkBioQrgJFg9ts8HuUucxawXua4WG+z5Iik9AL6+0PAzZMqXEX
|
||||
Rjyn6JVGm8IAOiCCxLN7uySYlYqYxO3yMR0Q5w==
|
||||
Private-Lines: 2
|
||||
AAAAQUO5dO9G7i0bxGTP0zV3eIwv5g0NhrQJfW/bMHS6XWwaxdpr+QZ+DbBJVzZP
|
||||
wYC0wLMW4bJAf+kjqUnj4wGocoTe
|
||||
Private-MAC: d67001d47e13c43dc8bdb9c68a25356a96c1c4a6714f3c5a1836fca646b78b54
|
||||
@@ -45,7 +45,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
||||
if (fcmToken) {
|
||||
setpollInterval(0);
|
||||
} else {
|
||||
setpollInterval(60000);
|
||||
setpollInterval(90000);
|
||||
}
|
||||
}, [fcmToken]);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -44,7 +44,7 @@ function LogLevelHierarchy(level) {
|
||||
return "orange";
|
||||
case "INFO":
|
||||
return "blue";
|
||||
case "WARNING":
|
||||
case "WARN":
|
||||
return "yellow";
|
||||
case "ERROR":
|
||||
return "red";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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 dayjs from "../../utils/day";
|
||||
import { fuzzyMatchDate } from "./formats.js";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import dayjs from "../../utils/day";
|
||||
import { fuzzyMatchDate } from "./formats.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -20,6 +20,7 @@ const DateTimePicker = ({
|
||||
onlyFuture,
|
||||
onlyToday,
|
||||
isDateOnly = false,
|
||||
isSeparatedTime = false,
|
||||
bodyshop,
|
||||
...restProps
|
||||
}) => {
|
||||
@@ -28,9 +29,8 @@ const DateTimePicker = ({
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newDate) => {
|
||||
if (!newDate) return;
|
||||
if (onChange) {
|
||||
onChange(bodyshop?.timezone ? dayjs(newDate).tz(bodyshop.timezone, true) : newDate);
|
||||
onChange(bodyshop?.timezone && newDate ? dayjs(newDate).tz(bodyshop.timezone, true) : newDate);
|
||||
}
|
||||
setIsManualInput(false);
|
||||
},
|
||||
@@ -88,24 +88,57 @@ const DateTimePicker = ({
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -117,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 connect(mapStateToProps, null)(DateTimePicker);
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,7 @@ export function TimeTicketList({
|
||||
extra
|
||||
}) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
sortedInfo: { columnKey: 'date', order: 'descend' },
|
||||
filteredInfo: { text: "" }
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { gql } from "@apollo/client";
|
||||
|
||||
export const GET_ALL_JOBLINES_BY_PK = gql`
|
||||
query GET_ALL_JOBLINES_BY_PK($id: uuid!) {
|
||||
joblines(where: { jobid: { _eq: $id } }, order_by: { line_no: asc }) {
|
||||
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }, order_by: { line_no: asc }) {
|
||||
id
|
||||
line_no
|
||||
unq_seq
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -71,7 +71,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
...logs,
|
||||
{
|
||||
timestamp: new Date(),
|
||||
level: "WARNING",
|
||||
level: "WARN",
|
||||
message: "Reconnected to CDK Export Service"
|
||||
}
|
||||
];
|
||||
@@ -125,7 +125,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
];
|
||||
@@ -175,7 +175,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
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
14
docker-build.ps1
Normal file
14
docker-build.ps1
Normal file
@@ -0,0 +1,14 @@
|
||||
# Stop and remove all containers, images, and networks from the Compose file
|
||||
docker compose down --rmi all
|
||||
|
||||
# Prune all unused Docker objects including volumes
|
||||
docker system prune --all --volumes --force
|
||||
|
||||
# Prune unused build cache
|
||||
docker builder prune --all --force
|
||||
|
||||
# Prune all unused volumes
|
||||
docker volume prune --all --force
|
||||
|
||||
# Rebuild and start the containers
|
||||
docker compose up --build
|
||||
16
docker-build.sh
Normal file
16
docker-build.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Stop and remove all containers, images, and networks from the Compose file
|
||||
docker compose down --rmi all
|
||||
|
||||
# Prune all unused Docker objects including volumes
|
||||
docker system prune --all --volumes --force
|
||||
|
||||
# Prune unused build cache
|
||||
docker builder prune --all --force
|
||||
|
||||
# Prune all unused volumes
|
||||
docker volume prune --all --force
|
||||
|
||||
# Rebuild and start the containers
|
||||
docker compose up --build
|
||||
@@ -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
|
||||
@@ -114,8 +114,9 @@ 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 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
|
||||
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,29 @@ 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/io-ftp-test.key.pub:/home/user/.ssh/keys/io-ftp-test.key.pub:ro # Mount the SSH public key as authorized_keys
|
||||
# - ./upload:/home/user/upload # Mount a local directory for SFTP uploads
|
||||
# environment:
|
||||
# # - SFTP_USERS=user::1000::upload
|
||||
# - 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 );
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX idx_timetickets_date ON timetickets (date );
|
||||
@@ -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);
|
||||
7
hasura/migrations/1730517308367_run_sql_migration/up.sql
Normal file
7
hasura/migrations/1730517308367_run_sql_migration/up.sql
Normal 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);
|
||||
@@ -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);
|
||||
1
hasura/migrations/1730518121867_run_sql_migration/up.sql
Normal file
1
hasura/migrations/1730518121867_run_sql_migration/up.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE INDEX idx_exportlog_createdat_desc ON exportlog (created_at desc);
|
||||
@@ -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;
|
||||
2
hasura/migrations/1730521661838_run_sql_migration/up.sql
Normal file
2
hasura/migrations/1730521661838_run_sql_migration/up.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
CREATE index idx_messages_unread_agg ON messages (read, isoutbound)
|
||||
WHERE read = false AND isoutbound = false;
|
||||
10
hasura/migrations/1731471670370_run_sql_migration/down.sql
Normal file
10
hasura/migrations/1731471670370_run_sql_migration/down.sql
Normal 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);
|
||||
8
hasura/migrations/1731471670370_run_sql_migration/up.sql
Normal file
8
hasura/migrations/1731471670370_run_sql_migration/up.sql
Normal 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);
|
||||
1297
package-lock.json
generated
1297
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
@@ -63,7 +64,7 @@
|
||||
"soap": "^1.1.5",
|
||||
"socket.io": "^4.8.0",
|
||||
"socket.io-adapter": "^2.5.5",
|
||||
"ssh2-sftp-client": "^10.0.3",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
"twilio": "^4.23.0",
|
||||
"uuid": "^10.0.0",
|
||||
"winston": "^3.15.0",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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]
|
||||
? {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
76
server/alerts/alertcheck.js
Normal file
76
server/alerts/alertcheck.js
Normal 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 };
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -24,9 +25,12 @@ const ftpSetup = {
|
||||
port: process.env.AUTOHOUSE_PORT,
|
||||
username: process.env.AUTOHOUSE_USER,
|
||||
password: process.env.AUTOHOUSE_PASSWORD,
|
||||
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
|
||||
debug:
|
||||
process.env.NODE_ENV !== "production"
|
||||
? (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"]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,160 +40,176 @@ exports.default = async (req, res) => {
|
||||
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-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") })
|
||||
});
|
||||
logger.log("autohouse-start", "DEBUG", "api", null, null);
|
||||
const allXMLResults = [];
|
||||
const allErrors = [];
|
||||
|
||||
const autoHouseObject = {
|
||||
AutoHouseExport: {
|
||||
RepairOrder: jobs.map((j) =>
|
||||
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
|
||||
erroredJobs.push({ job: job, error: error.toString() });
|
||||
})
|
||||
)
|
||||
}
|
||||
};
|
||||
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
|
||||
|
||||
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))
|
||||
});
|
||||
}
|
||||
const shopsToProcess =
|
||||
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||
logger.log("autohouse-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||
|
||||
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
|
||||
)}
|
||||
`
|
||||
});
|
||||
if (shopsToProcess.length === 0) {
|
||||
logger.log("autohouse-shopsToProcess-empty", "DEBUG", "api", null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let sftp = new Client();
|
||||
sftp.on("error", (errors) =>
|
||||
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
|
||||
...errors
|
||||
})
|
||||
);
|
||||
await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);
|
||||
|
||||
await sendServerEmail({
|
||||
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
|
||||
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||
allXMLResults.map((x) => ({
|
||||
imexshopid: x.imexshopid,
|
||||
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 processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
|
||||
for (const bodyshop of shopsToProcess) {
|
||||
const erroredJobs = [];
|
||||
try {
|
||||
//Connect to the FTP and upload all.
|
||||
logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||
shopname: bodyshop.shopname
|
||||
});
|
||||
|
||||
await sftp.connect(ftpSetup);
|
||||
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") })
|
||||
});
|
||||
|
||||
for (const xmlObj of allxmlsToUpload) {
|
||||
logger.log("autohouse-sftp-upload", "DEBUG", "api", null, {
|
||||
filename: xmlObj.filename
|
||||
});
|
||||
const autoHouseObject = {
|
||||
AutoHouseExport: {
|
||||
RepairOrder: jobs.map((j) =>
|
||||
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
|
||||
erroredJobs.push({ job: job, error: error.toString() });
|
||||
})
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
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 xmlObj = {
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
xml: builder.create({}, autoHouseObject).end({ allowEmptyTags: true }),
|
||||
filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml`,
|
||||
count: autoHouseObject.AutoHouseExport.RepairOrder.length
|
||||
};
|
||||
|
||||
if (skipUpload) {
|
||||
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
||||
} else {
|
||||
await uploadViaSFTP(xmlObj);
|
||||
}
|
||||
|
||||
allXMLResults.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
autohouseid: bodyshop.autohouseid,
|
||||
count: xmlObj.count,
|
||||
filename: xmlObj.filename,
|
||||
result: xmlObj.result
|
||||
});
|
||||
|
||||
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(xmlObj) {
|
||||
const sftp = new Client();
|
||||
sftp.on("error", (errors) =>
|
||||
logger.log("autohouse-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||
error: errors.message,
|
||||
stack: errors.stack
|
||||
})
|
||||
);
|
||||
try {
|
||||
//Connect to the FTP and upload all.
|
||||
await sftp.connect(ftpSetup);
|
||||
|
||||
try {
|
||||
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
|
||||
logger.log("autohouse-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
|
||||
imexshopid: xmlObj.imexshopid,
|
||||
filename: xmlObj.filename,
|
||||
result: xmlObj.result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log("autohouse-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||
filename: xmlObj.filename,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("autohouse-sftp-error", "ERROR", "api", xmlObj.bodyshopid, { error: error.message, stack: error.stack });
|
||||
throw error;
|
||||
} finally {
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
//Level 2
|
||||
@@ -287,8 +307,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 +621,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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,140 +17,126 @@ const ftpSetup = {
|
||||
port: process.env.CHATTER_PORT,
|
||||
username: process.env.CHATTER_USER,
|
||||
privateKey: null,
|
||||
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
|
||||
debug:
|
||||
process.env.NODE_ENV !== "production"
|
||||
? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
|
||||
: () => {},
|
||||
algorithms: {
|
||||
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
||||
}
|
||||
};
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
// Only process if in production environment.
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
res.sendStatus(403);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 allChatterObjects = [];
|
||||
const allErrors = [];
|
||||
|
||||
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 { 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 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();
|
||||
await processBatch(shopsToProcess, start, end, allChatterObjects, allErrors);
|
||||
|
||||
//Connect to the FTP and upload all.
|
||||
await sftp.connect(ftpSetup);
|
||||
const csvToUpload = {
|
||||
count: allChatterObjects.length,
|
||||
csv: converter.json2csv(allChatterObjects, { emptyFieldValue: "" }),
|
||||
filename: `IMEX_ONLINE_solicitation_${moment().format("YYYYMMDD")}.csv`
|
||||
};
|
||||
|
||||
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();
|
||||
if (skipUpload) {
|
||||
await fs.promises.writeFile(`./logs/${csvToUpload.filename}`, csvToUpload.csv);
|
||||
} else {
|
||||
await uploadViaSFTP(csvToUpload);
|
||||
}
|
||||
sendServerEmail({
|
||||
|
||||
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 })),
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\n
|
||||
Uploaded:\n${JSON.stringify({ filename: csvToUpload.filename, count: csvToUpload.count, result: csvToUpload.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(shopsToProcess, start, end, allChatterObjects, allErrors) {
|
||||
for (const bodyshop of shopsToProcess) {
|
||||
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
|
||||
};
|
||||
});
|
||||
allChatterObjects.push(...chatterObject);
|
||||
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 +146,44 @@ 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(csvToUpload) {
|
||||
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 });
|
||||
|
||||
try {
|
||||
csvToUpload.result = await sftp.put(Buffer.from(csvToUpload.csv), `${csvToUpload.filename}`);
|
||||
logger.log("chatter-sftp-upload", "DEBUG", "api", null, {
|
||||
filename: csvToUpload.filename,
|
||||
result: csvToUpload.result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log("chatter-sftp-upload-error", "ERROR", "api", null, {
|
||||
filename: csvToUpload.filename,
|
||||
error: csvToUpload.message,
|
||||
stack: csvToUpload.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("chatter-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
||||
throw error;
|
||||
} finally {
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,12 @@ const ftpSetup = {
|
||||
port: process.env.CLAIMSCORP_PORT,
|
||||
username: process.env.CLAIMSCORP_USER,
|
||||
password: process.env.CLAIMSCORP_PASSWORD,
|
||||
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
|
||||
debug:
|
||||
process.env.NODE_ENV !== "production"
|
||||
? (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"]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,164 +39,183 @@ exports.default = async (req, res) => {
|
||||
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-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") })
|
||||
});
|
||||
logger.log("claimscorp-start", "DEBUG", "api", null, null);
|
||||
const allXMLResults = [];
|
||||
const allErrors = [];
|
||||
|
||||
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() });
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
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
|
||||
|
||||
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))
|
||||
});
|
||||
}
|
||||
const shopsToProcess =
|
||||
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||
logger.log("claimscorp-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||
|
||||
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
|
||||
)}
|
||||
`
|
||||
});
|
||||
if (shopsToProcess.length === 0) {
|
||||
logger.log("claimscorp-shopsToProcess-empty", "DEBUG", "api", null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let sftp = new Client();
|
||||
sftp.on("error", (errors) =>
|
||||
logger.log("claimscorp-sftp-error", "ERROR", "api", null, {
|
||||
...errors
|
||||
})
|
||||
);
|
||||
await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);
|
||||
|
||||
await sendServerEmail({
|
||||
subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`,
|
||||
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||
allXMLResults.map((x) => ({
|
||||
imexshopid: x.imexshopid,
|
||||
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 processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
|
||||
for (const bodyshop of shopsToProcess) {
|
||||
const erroredJobs = [];
|
||||
try {
|
||||
//Connect to the FTP and upload all.
|
||||
logger.log("claimscorp-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||
shopname: bodyshop.shopname
|
||||
});
|
||||
|
||||
await sftp.connect(ftpSetup);
|
||||
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") })
|
||||
});
|
||||
|
||||
for (const xmlObj of allxmlsToUpload) {
|
||||
logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, {
|
||||
filename: xmlObj.filename
|
||||
});
|
||||
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() });
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 xmlObj = {
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
xml: builder.create({}, claimsCorpObject).end({ allowEmptyTags: true }),
|
||||
filename: `${bodyshop.claimscorpid}-${moment().format("YYYYMMDDTHHMMss")}.xml`,
|
||||
count: claimsCorpObject.DataFeed.ShopInfo.RO.length
|
||||
};
|
||||
|
||||
if (skipUpload) {
|
||||
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
||||
} else {
|
||||
await uploadViaSFTP(xmlObj);
|
||||
}
|
||||
|
||||
allXMLResults.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
claimscorpid: bodyshop.claimscorpid,
|
||||
count: xmlObj.count,
|
||||
filename: xmlObj.filename,
|
||||
result: xmlObj.result
|
||||
});
|
||||
|
||||
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(xmlObj) {
|
||||
const sftp = new Client();
|
||||
sftp.on("error", (errors) =>
|
||||
logger.log("claimscorp-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||
error: errors.message,
|
||||
stack: errors.stack
|
||||
})
|
||||
);
|
||||
try {
|
||||
//Connect to the FTP and upload all.
|
||||
await sftp.connect(ftpSetup);
|
||||
|
||||
try {
|
||||
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
|
||||
logger.log("claimscorp-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
|
||||
imexshopid: xmlObj.imexshopid,
|
||||
filename: xmlObj.filename,
|
||||
result: xmlObj.result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log("claimscorp-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||
filename: xmlObj.filename,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("claimscorp-sftp-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
//Level 2
|
||||
@@ -445,10 +467,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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,15 +16,17 @@ 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,
|
||||
port: process.env.KAIZEN_PORT,
|
||||
username: process.env.KAIZEN_USER,
|
||||
password: process.env.KAIZEN_PASSWORD,
|
||||
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
|
||||
debug:
|
||||
process.env.NODE_ENV !== "production"
|
||||
? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
|
||||
: () => {},
|
||||
algorithms: {
|
||||
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
||||
}
|
||||
@@ -36,167 +38,178 @@ exports.default = async (req, res) => {
|
||||
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-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") })
|
||||
});
|
||||
logger.log("kaizen-start", "DEBUG", "api", null, null);
|
||||
const allXMLResults = [];
|
||||
const allErrors = [];
|
||||
|
||||
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() });
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
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
|
||||
|
||||
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))
|
||||
});
|
||||
}
|
||||
const shopsToProcess =
|
||||
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||
logger.log("kaizen-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||
|
||||
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
|
||||
)}
|
||||
`
|
||||
});
|
||||
if (shopsToProcess.length === 0) {
|
||||
logger.log("kaizen-shopsToProcess-empty", "DEBUG", "api", null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let sftp = new Client();
|
||||
sftp.on("error", (errors) =>
|
||||
logger.log("kaizen-sftp-error", "ERROR", "api", null, {
|
||||
...errors
|
||||
})
|
||||
);
|
||||
await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);
|
||||
|
||||
await sendServerEmail({
|
||||
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
|
||||
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||
allXMLResults.map((x) => ({
|
||||
imexshopid: x.imexshopid,
|
||||
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 processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
|
||||
for (const bodyshop of shopsToProcess) {
|
||||
const erroredJobs = [];
|
||||
try {
|
||||
//Connect to the FTP and upload all.
|
||||
logger.log("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||
shopname: bodyshop.shopname
|
||||
});
|
||||
|
||||
await sftp.connect(ftpSetup);
|
||||
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") })
|
||||
});
|
||||
|
||||
for (const xmlObj of allxmlsToUpload) {
|
||||
logger.log("kaizen-sftp-upload", "DEBUG", "api", null, {
|
||||
filename: xmlObj.filename
|
||||
});
|
||||
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() });
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 xmlObj = {
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
xml: builder.create({}, kaizenObject).end({ allowEmptyTags: true }),
|
||||
filename: `${bodyshop.shopname}-${moment().format("YYYYMMDDTHHMMss")}.xml`,
|
||||
count: kaizenObject.DataFeed.ShopInfo.Jobs.length
|
||||
};
|
||||
|
||||
if (skipUpload) {
|
||||
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
||||
} else {
|
||||
await uploadViaSFTP(xmlObj);
|
||||
}
|
||||
|
||||
allXMLResults.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
count: xmlObj.count,
|
||||
filename: xmlObj.filename,
|
||||
result: xmlObj.result
|
||||
});
|
||||
|
||||
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(xmlObj) {
|
||||
const sftp = new Client();
|
||||
sftp.on("error", (errors) =>
|
||||
logger.log("kaizen-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||
error: errors.message,
|
||||
stack: errors.stack
|
||||
})
|
||||
);
|
||||
try {
|
||||
//Connect to the FTP and upload all.
|
||||
await sftp.connect(ftpSetup);
|
||||
|
||||
try {
|
||||
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
|
||||
logger.log("kaizen-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
|
||||
imexshopid: xmlObj.imexshopid,
|
||||
filename: xmlObj.filename,
|
||||
result: xmlObj.result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log("kaizen-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||
filename: xmlObj.filename,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("kaizen-sftp-error", "ERROR", "api", xmlObj.bodyshopid, { error: error.message, stack: error.stack });
|
||||
throw error;
|
||||
} finally {
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
//Level 2
|
||||
@@ -420,10 +433,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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,11 @@ const validateFirebaseIdTokenMiddleware = async (req, res, next) => {
|
||||
(!req.headers.authorization || !req.headers.authorization.startsWith("Bearer ")) &&
|
||||
!(req.cookies && req.cookies.__session)
|
||||
) {
|
||||
console.error("Unauthorized attempt. No authorization provided.");
|
||||
logger.log("api-authorization-call", "warn", req?.user?.email, null, {
|
||||
type: "unauthorized",
|
||||
path: req.path,
|
||||
body: req.body
|
||||
});
|
||||
return res.status(403).send("Unauthorized");
|
||||
}
|
||||
|
||||
@@ -32,10 +36,10 @@ const validateFirebaseIdTokenMiddleware = async (req, res, next) => {
|
||||
idToken = req.cookies.__session;
|
||||
} else {
|
||||
// No cookie
|
||||
console.error("Unauthorized attempt. No cookie provided.");
|
||||
logger.log("api-unauthorized-call", "WARN", null, null, {
|
||||
req,
|
||||
type: "no-cookie"
|
||||
logger.log("api-unauthorized-call", "warn", null, null, {
|
||||
type: "unauthorized",
|
||||
path: req.path,
|
||||
body: req.body
|
||||
});
|
||||
|
||||
return res.status(403).send("Unauthorized");
|
||||
@@ -47,11 +51,11 @@ const validateFirebaseIdTokenMiddleware = async (req, res, next) => {
|
||||
req.user = decodedIdToken;
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.log("api-unauthorized-call", "WARN", null, null, {
|
||||
logger.log("api-unauthorized-call", "warn", null, null, {
|
||||
path: req.path,
|
||||
body: req.body,
|
||||
|
||||
type: "unauthroized",
|
||||
type: "unauthorized",
|
||||
...error
|
||||
});
|
||||
|
||||
|
||||
@@ -9,11 +9,9 @@ require("dotenv").config({
|
||||
});
|
||||
|
||||
exports.mixdataUpload = async (req, res) => {
|
||||
const { bodyshopid } = req.body;
|
||||
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
logger.log("job-mixdata-upload", "DEBUG", req.user.email, null, null);
|
||||
logger.log("job-mixdata-upload", "DEBUG", req?.user?.email, null, null);
|
||||
|
||||
try {
|
||||
for (const element of req.files) {
|
||||
@@ -23,7 +21,7 @@ exports.mixdataUpload = async (req, res) => {
|
||||
explicitArray: false
|
||||
});
|
||||
|
||||
logger.log("job-mixdata-parse", "DEBUG", req.user.email, inboundRequest);
|
||||
logger.log("job-mixdata-parse", "DEBUG", req?.user?.email, null, { inboundRequest });
|
||||
|
||||
const ScaleType = DetermineScaleType(inboundRequest);
|
||||
const RoNumbersFromInboundRequest = GetListOfRos(inboundRequest, ScaleType);
|
||||
@@ -61,7 +59,7 @@ exports.mixdataUpload = async (req, res) => {
|
||||
res.status(500).json(error);
|
||||
logger.log("job-mixdata-upload-error", "ERROR", null, null, {
|
||||
error: error.message,
|
||||
...error
|
||||
stack: error.stack
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -70,7 +68,7 @@ function DetermineScaleType(inboundRequest) {
|
||||
const ret = { type: "", verson: 0 };
|
||||
|
||||
//PPG Mix Data
|
||||
if (inboundRequest.PPG && inboundRequest.PPG.Header.Protocol.Name === "PPG") {
|
||||
if (inboundRequest?.PPG?.Header?.Protocol?.Name === "PPG") {
|
||||
return {
|
||||
type: inboundRequest.PPG.Header.Protocol.Name,
|
||||
company: "PPG",
|
||||
@@ -80,13 +78,13 @@ function DetermineScaleType(inboundRequest) {
|
||||
}
|
||||
|
||||
function GetListOfRos(inboundRequest, ScaleType) {
|
||||
if (ScaleType.company === "PPG" && ScaleType.version === "1.3.0") {
|
||||
if (ScaleType?.company === "PPG" && ScaleType?.version === "1.3.0") {
|
||||
return inboundRequest.PPG.MixDataInterface.ROData.RepairOrders.RO.map((r) => r.RONumber);
|
||||
}
|
||||
}
|
||||
|
||||
function GenerateMixDataArray(inboundRequest, ScaleType, jobHash) {
|
||||
if (ScaleType.company === "PPG" && ScaleType.version === "1.3.0") {
|
||||
if (ScaleType?.company === "PPG" && ScaleType?.version === "1.3.0") {
|
||||
return inboundRequest.PPG.MixDataInterface.ROData.RepairOrders.RO.map((r) => {
|
||||
return {
|
||||
jobid: jobHash[r.RONumber]?.jobid,
|
||||
|
||||
@@ -45,12 +45,12 @@ exports.payall = async function (req, res) {
|
||||
const path = diffParser(diff);
|
||||
|
||||
if (diff.op === "add") {
|
||||
console.log(Object.keys(diff.val));
|
||||
// console.log(Object.keys(diff.val));
|
||||
if (typeof diff.val === "object" && Object.keys(diff.val).length > 1) {
|
||||
//Multiple values to add.
|
||||
Object.keys(diff.val).forEach((key) => {
|
||||
console.log("Hours", diff.val[key][Object.keys(diff.val[key])[0]]);
|
||||
console.log("Rate", Object.keys(diff.val[key])[0]);
|
||||
// console.log("Hours", diff.val[key][Object.keys(diff.val[key])[0]]);
|
||||
// console.log("Rate", Object.keys(diff.val[key])[0]);
|
||||
ticketsToInsert.push({
|
||||
task_name: "Pay All",
|
||||
jobid: job.id,
|
||||
|
||||
@@ -12,9 +12,10 @@ const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebas
|
||||
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
||||
const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
||||
const { canvastest } = require("../render/canvas-handler");
|
||||
const { alertCheck } = require("../alerts/alertcheck");
|
||||
|
||||
//Test route to ensure Express is responding.
|
||||
router.get("/test", async function (req, res) {
|
||||
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
|
||||
const commit = require("child_process").execSync("git rev-parse --short HEAD");
|
||||
// console.log(app.get('trust proxy'));
|
||||
// console.log("remoteAddress", req.socket.remoteAddress);
|
||||
@@ -31,6 +32,32 @@ router.get("/test", async function (req, res) {
|
||||
res.status(200).send(`OK - ${commit}`);
|
||||
});
|
||||
|
||||
router.get("/test-logs", eventAuthorizationMiddleware, (req, res) => {
|
||||
const { logger } = req;
|
||||
// // Test 1: Log with a message that exceeds the size limit, triggering an upload to S3.
|
||||
const largeMessage = "A".repeat(256 * 1024 + 1); // Message larger than the log size limit
|
||||
logger.log(largeMessage, "error", "user123", null, { detail: "large log entry" });
|
||||
|
||||
// Test 2: Log with a message that is within the size limit, should log directly using winston.
|
||||
const smallMessage = "A small log message";
|
||||
logger.log(smallMessage, "info", "user123", null, { detail: "small log entry" });
|
||||
|
||||
// Test 3: Log with the `upload` flag set to `true`, forcing the log to be uploaded to S3.
|
||||
logger.log(
|
||||
"This log will be uploaded to S3 regardless of size",
|
||||
"warning",
|
||||
"user123",
|
||||
null,
|
||||
{ detail: "upload log" },
|
||||
true
|
||||
);
|
||||
|
||||
// Test 4: Log with a message that doesn't exceed the size limit and doesn't require an upload.
|
||||
logger.log("Normal log entry", "debug", "user123", { id: 4 }, { detail: "normal log entry" });
|
||||
|
||||
return res.status(500).send("Logs tested.");
|
||||
});
|
||||
|
||||
// Search
|
||||
router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search);
|
||||
router.post("/opensearch", eventAuthorizationMiddleware, os.handler);
|
||||
@@ -53,4 +80,7 @@ router.post("/taskHandler", validateFirebaseIdTokenMiddleware, taskHandler.taskH
|
||||
// Canvas Test
|
||||
router.post("/canvastest", validateFirebaseIdTokenMiddleware, canvastest);
|
||||
|
||||
// Alert Check
|
||||
router.post("/alertcheck", eventAuthorizationMiddleware, alertCheck);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -49,7 +49,7 @@ exports.job = async (req, res) => {
|
||||
if (bucketId) {
|
||||
load.productionTotal[bucketId].count = load.productionTotal[bucketId].count + 1;
|
||||
} else {
|
||||
console.log("Uh oh, this job doesn't fit in a bucket!", item);
|
||||
// console.log("Uh oh, this job doesn't fit in a bucket!", item);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -254,7 +254,7 @@ const CalculateLoad = (currentLoad, buckets, jobsIn, jobsOut) => {
|
||||
if (bucketId) {
|
||||
newLoad[bucketId].count = newLoad[bucketId].count + 1;
|
||||
} else {
|
||||
console.log("[Util Arr Job]Uh oh, this job doesn't fit in a bucket!", job);
|
||||
// console.log("[Util Arr Job]Uh oh, this job doesn't fit in a bucket!", job);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -263,10 +263,10 @@ const CalculateLoad = (currentLoad, buckets, jobsIn, jobsOut) => {
|
||||
if (bucketId) {
|
||||
newLoad[bucketId].count = newLoad[bucketId].count - 1;
|
||||
if (newLoad[bucketId].count < 0) {
|
||||
console.log("***ERROR: NEGATIVE LOAD Bucket =>", bucketId, job);
|
||||
// console.log("***ERROR: NEGATIVE LOAD Bucket =>", bucketId, job);
|
||||
}
|
||||
} else {
|
||||
console.log("[Util Out Job]Uh oh, this job doesn't fit in a bucket!", job);
|
||||
// console.log("[Util Out Job]Uh oh, this job doesn't fit in a bucket!", job);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const processor = async (req, res) => {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
// console.log("error", error);
|
||||
res.status(400).send(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,10 +6,11 @@ const client = require("../graphql-client/graphql-client").client;
|
||||
const emailer = require("../email/sendemail");
|
||||
const moment = require("moment-timezone");
|
||||
const converter = require("json-2-csv");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
exports.taskHandler = async (req, res) => {
|
||||
try {
|
||||
const { bodyshopid, query, variables, text, to, subject, timezone } = req.body;
|
||||
const { query, variables, text, to, subject, timezone } = req.body;
|
||||
|
||||
//Check the variables to see if they are an object.
|
||||
Object.keys(variables).forEach((key) => {
|
||||
@@ -32,8 +33,10 @@ exports.taskHandler = async (req, res) => {
|
||||
text,
|
||||
attachments: [{ filename: "query.csv", content: csv }]
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Errors sending CSV Email.");
|
||||
.catch((error) => {
|
||||
logger.log("Tasks - Error sending CSV EMAIL", "error", req?.user?.email, null, {
|
||||
error
|
||||
});
|
||||
});
|
||||
|
||||
return res.status(200).send(csv);
|
||||
|
||||
@@ -44,4 +44,10 @@ function InstanceManager({ args, instance, debug, executeFunction, rome, promana
|
||||
return propToReturn === undefined ? null : propToReturn;
|
||||
}
|
||||
|
||||
exports.InstanceRegion = () =>
|
||||
InstanceManager({
|
||||
imex: "ca-central-1",
|
||||
rome: "us-east-2"
|
||||
});
|
||||
|
||||
exports.default = InstanceManager;
|
||||
|
||||
@@ -8,7 +8,46 @@ const InstanceManager = require("../utils/instanceMgr").default;
|
||||
const winston = require("winston");
|
||||
const WinstonCloudWatch = require("winston-cloudwatch");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
const { networkInterfaces } = require("node:os");
|
||||
const { networkInterfaces, hostname } = require("node:os");
|
||||
const { uploadFileToS3 } = require("./s3");
|
||||
const { v4 } = require("uuid");
|
||||
const { InstanceRegion } = require("./instanceMgr");
|
||||
|
||||
const LOG_LEVELS = {
|
||||
error: { level: 0, name: "error" },
|
||||
warn: { level: 1, name: "warn" },
|
||||
info: { level: 2, name: "info" },
|
||||
http: { level: 3, name: "http" },
|
||||
verbose: { level: 4, name: "verbose" },
|
||||
debug: { level: 5, name: "debug" },
|
||||
silly: { level: 6, name: "silly" }
|
||||
};
|
||||
|
||||
const LOG_LENGTH_LIMIT = 256 * 1024; // 256KB
|
||||
|
||||
const S3_BUCKET_NAME = InstanceManager({
|
||||
imex: "imex-large-log",
|
||||
rome: "rome-large-log"
|
||||
});
|
||||
|
||||
const region = InstanceRegion();
|
||||
|
||||
const estimateLogSize = (logEntry) => {
|
||||
let estimatedSize = 0;
|
||||
for (const key in logEntry) {
|
||||
if (logEntry.hasOwnProperty(key)) {
|
||||
const value = logEntry[key];
|
||||
if (value === undefined || value === null) {
|
||||
estimatedSize += key.length; // Only count the key length if value is undefined or null
|
||||
} else {
|
||||
estimatedSize += key.length + (typeof value === "string" ? value.length : JSON.stringify(value).length);
|
||||
}
|
||||
}
|
||||
}
|
||||
return estimatedSize;
|
||||
};
|
||||
|
||||
const normalizeLevel = (level) => (level ? level.toLowerCase() : LOG_LEVELS.debug.name);
|
||||
|
||||
const createLogger = () => {
|
||||
try {
|
||||
@@ -18,19 +57,13 @@ const createLogger = () => {
|
||||
const winstonCloudwatchTransportDefaults = {
|
||||
logGroupName: logGroupName,
|
||||
awsOptions: {
|
||||
region: InstanceManager({
|
||||
imex: "ca-central-1",
|
||||
rome: "us-east-2"
|
||||
})
|
||||
region
|
||||
},
|
||||
jsonMessage: true
|
||||
};
|
||||
|
||||
if (isLocal) {
|
||||
winstonCloudwatchTransportDefaults.awsOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
console.log(
|
||||
`Winston Transports set to LocalStack end point: ${winstonCloudwatchTransportDefaults.awsOptions.endpoint}`
|
||||
);
|
||||
}
|
||||
|
||||
const levelFilter = (levels) => {
|
||||
@@ -43,19 +76,22 @@ const createLogger = () => {
|
||||
})();
|
||||
};
|
||||
|
||||
const getPrivateIP = () => {
|
||||
const getHostNameOrIP = () => {
|
||||
// Try to get the hostname first
|
||||
const hostName = hostname();
|
||||
if (hostName) return hostName;
|
||||
|
||||
const interfaces = networkInterfaces();
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const iface of interfaces[name]) {
|
||||
// Find an IPv4 address that's not internal (like localhost)
|
||||
if (iface.family === "IPv4" && !iface.internal) {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "127.0.0.1";
|
||||
};
|
||||
|
||||
const createProductionTransport = (level, logStreamName, filters) => {
|
||||
return new WinstonCloudWatch({
|
||||
level,
|
||||
@@ -65,7 +101,7 @@ const createLogger = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const hostname = process.env.HOSTNAME || getPrivateIP();
|
||||
const internalHostname = process.env.HOSTNAME || getHostNameOrIP();
|
||||
|
||||
const getDevelopmentTransports = () => [
|
||||
new winston.transports.Console({
|
||||
@@ -74,7 +110,7 @@ const createLogger = () => {
|
||||
winston.format.colorize(),
|
||||
winston.format.timestamp(),
|
||||
winston.format.printf(({ level, message, timestamp, user, record, meta }) => {
|
||||
const hostnameColor = `\x1b[34m${hostname}\x1b[0m`; // Blue
|
||||
const hostnameColor = `\x1b[34m${internalHostname}\x1b[0m`; // Blue
|
||||
const timestampColor = `\x1b[36m${timestamp}\x1b[0m`; // Cyan
|
||||
const labelColor = "\x1b[33m"; // Yellow
|
||||
const separatorColor = "\x1b[35m|\x1b[0m"; // Magenta for separators
|
||||
@@ -106,15 +142,72 @@ const createLogger = () => {
|
||||
: [...getDevelopmentTransports(), ...getProductionTransports()]
|
||||
});
|
||||
|
||||
const log = (message, type, user, record, meta) => {
|
||||
winstonLogger.log({
|
||||
level: type.toLowerCase(),
|
||||
if (isLocal) {
|
||||
winstonLogger.debug(
|
||||
`CloudWatch set to LocalStack end point: ${winstonCloudwatchTransportDefaults.awsOptions.endpoint}`
|
||||
);
|
||||
}
|
||||
|
||||
const log = (message, type, user, record, meta, upload) => {
|
||||
const logEntry = {
|
||||
level: normalizeLevel(type),
|
||||
message,
|
||||
user,
|
||||
record,
|
||||
hostname,
|
||||
hostname: internalHostname,
|
||||
meta
|
||||
});
|
||||
};
|
||||
|
||||
const uploadLogToS3 = (logEntry, message, type, user) => {
|
||||
const uniqueId = v4();
|
||||
const dateTimeString = new Date().toISOString().replace(/:/g, "-");
|
||||
const envName = process.env?.NODE_ENV ? process.env.NODE_ENV : "";
|
||||
const logStreamName = `${envName}-${internalHostname}-${dateTimeString}-${uniqueId}.json`;
|
||||
const logString = JSON.stringify(logEntry);
|
||||
const webPath = isLocal
|
||||
? `https://${S3_BUCKET_NAME}.s3.localhost.localstack.cloud:4566/${logStreamName}`
|
||||
: `https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/${logStreamName}`;
|
||||
|
||||
uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: logStreamName, content: logString })
|
||||
.then(() => {
|
||||
log("A log file has been uploaded to S3", "info", "S3", null, {
|
||||
logStreamName,
|
||||
webPath,
|
||||
message: message?.slice(0, 200),
|
||||
type,
|
||||
user
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
log("Error in S3 Upload", "error", "S3", null, {
|
||||
logStreamName,
|
||||
webPath,
|
||||
message: message?.slice(0, 100),
|
||||
type,
|
||||
user,
|
||||
errorMessage: err?.message?.slice(0, 100)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const checkAndUploadLog = () => {
|
||||
const estimatedSize = estimateLogSize(logEntry);
|
||||
|
||||
if (estimatedSize > LOG_LENGTH_LIMIT * 0.9 || estimatedSize > LOG_LENGTH_LIMIT) {
|
||||
uploadLogToS3(logEntry, message, type, user);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Upload log immediately if upload is true, otherwise check the log size.
|
||||
if (upload) {
|
||||
uploadLogToS3(logEntry, message, type, user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkAndUploadLog()) return;
|
||||
|
||||
winstonLogger.log(logEntry);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -125,7 +218,8 @@ const createLogger = () => {
|
||||
console.error("Error setting up enhanced Logger, defaulting to console.: " + e?.message || "");
|
||||
return {
|
||||
log: console.log,
|
||||
logger: console.log
|
||||
logger: console.log,
|
||||
LOG_LEVELS
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
109
server/utils/s3.js
Normal file
109
server/utils/s3.js
Normal file
@@ -0,0 +1,109 @@
|
||||
const {
|
||||
S3Client,
|
||||
PutObjectCommand,
|
||||
GetObjectCommand,
|
||||
ListObjectsV2Command,
|
||||
DeleteObjectCommand,
|
||||
CopyObjectCommand
|
||||
} = require("@aws-sdk/client-s3");
|
||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const { InstanceRegion } = require("./instanceMgr");
|
||||
const { isString, isEmpty } = require("lodash");
|
||||
|
||||
const createS3Client = () => {
|
||||
const S3Options = {
|
||||
region: InstanceRegion(),
|
||||
credentials: defaultProvider()
|
||||
};
|
||||
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
if (isLocal) {
|
||||
S3Options.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`;
|
||||
S3Options.forcePathStyle = true; // Needed for LocalStack to avoid bucket name as hostname
|
||||
}
|
||||
|
||||
const s3Client = new S3Client(S3Options);
|
||||
|
||||
/**
|
||||
* Uploads a file to the specified S3 bucket and key.
|
||||
*/
|
||||
const uploadFileToS3 = async ({ bucketName, key, content, contentType }) => {
|
||||
const params = {
|
||||
Bucket: bucketName,
|
||||
Key: key,
|
||||
Body: content,
|
||||
ContentType: contentType ?? "application/json"
|
||||
};
|
||||
const command = new PutObjectCommand(params);
|
||||
return await s3Client.send(command);
|
||||
};
|
||||
|
||||
/**
|
||||
* Downloads a file from the specified S3 bucket and key.
|
||||
*/
|
||||
const downloadFileFromS3 = async ({ bucketName, key }) => {
|
||||
const params = { Bucket: bucketName, Key: key };
|
||||
const command = new GetObjectCommand(params);
|
||||
const data = await s3Client.send(command);
|
||||
return data.Body;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lists objects in the specified S3 bucket.
|
||||
*/
|
||||
const listFilesInS3Bucket = async (bucketName, prefix = "") => {
|
||||
const params = { Bucket: bucketName, Prefix: prefix };
|
||||
const command = new ListObjectsV2Command(params);
|
||||
const data = await s3Client.send(command);
|
||||
return data.Contents || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a file from the specified S3 bucket and key.
|
||||
*/
|
||||
const deleteFileFromS3 = async ({ bucketName, key }) => {
|
||||
const params = { Bucket: bucketName, Key: key };
|
||||
const command = new DeleteObjectCommand(params);
|
||||
return await s3Client.send(command);
|
||||
};
|
||||
|
||||
/**
|
||||
* Copies a file within S3 from a source bucket/key to a destination bucket/key.
|
||||
*/
|
||||
const copyFileInS3 = async ({ sourceBucket, sourceKey, destinationBucket, destinationKey }) => {
|
||||
const params = {
|
||||
CopySource: `/${sourceBucket}/${sourceKey}`,
|
||||
Bucket: destinationBucket,
|
||||
Key: destinationKey
|
||||
};
|
||||
const command = new CopyObjectCommand(params);
|
||||
return await s3Client.send(command);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a file exists in the specified S3 bucket and key.
|
||||
*/
|
||||
const fileExistsInS3 = async ({ bucketName, key }) => {
|
||||
try {
|
||||
await downloadFileFromS3({ bucketName, key });
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.name === "NoSuchKey" || error.name === "NotFound") {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
return {
|
||||
uploadFileToS3,
|
||||
downloadFileFromS3,
|
||||
listFilesInS3Bucket,
|
||||
deleteFileFromS3,
|
||||
copyFileInS3,
|
||||
fileExistsInS3,
|
||||
...s3Client
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = createS3Client();
|
||||
@@ -41,7 +41,8 @@ const redisSocketEvents = ({
|
||||
|
||||
// Register Socket Events
|
||||
const registerSocketEvents = (socket) => {
|
||||
createLogEvent(socket, "debug", `Registering RedisIO Socket Events.`);
|
||||
// Uncomment for further testing
|
||||
// createLogEvent(socket, "debug", `Registering RedisIO Socket Events.`);
|
||||
|
||||
// Token Update Events
|
||||
const registerUpdateEvents = (socket) => {
|
||||
@@ -54,12 +55,13 @@ const redisSocketEvents = ({
|
||||
// If We ever want to persist user Data across workers
|
||||
// await setSessionData(socket.id, "user", user);
|
||||
|
||||
createLogEvent(socket, "debug", "Token updated successfully");
|
||||
// Uncomment for further testing
|
||||
// createLogEvent(socket, "debug", "Token updated successfully");
|
||||
|
||||
socket.emit("token-updated", { success: true });
|
||||
} catch (error) {
|
||||
if (error.code === "auth/id-token-expired") {
|
||||
createLogEvent(socket, "WARNING", "Stale token received, waiting for new token");
|
||||
createLogEvent(socket, "warn", "Stale token received, waiting for new token");
|
||||
socket.emit("token-updated", {
|
||||
success: false,
|
||||
error: "Stale token."
|
||||
@@ -80,7 +82,7 @@ const redisSocketEvents = ({
|
||||
try {
|
||||
const room = getBodyshopRoom(bodyshopUUID);
|
||||
socket.join(room);
|
||||
createLogEvent(socket, "debug", `Client joined bodyshop room: ${room}`);
|
||||
// createLogEvent(socket, "debug", `Client joined bodyshop room: ${room}`);
|
||||
} catch (error) {
|
||||
createLogEvent(socket, "error", `Error joining room: ${error}`);
|
||||
}
|
||||
@@ -114,7 +116,9 @@ const redisSocketEvents = ({
|
||||
// Disconnect Events
|
||||
const registerDisconnectEvents = (socket) => {
|
||||
const disconnect = () => {
|
||||
createLogEvent(socket, "debug", `User disconnected.`);
|
||||
// Uncomment for further testing
|
||||
// createLogEvent(socket, "debug", `User disconnected.`);
|
||||
|
||||
const rooms = Array.from(socket.rooms).filter((room) => room !== socket.id);
|
||||
for (const room of rooms) {
|
||||
socket.leave(room);
|
||||
|
||||
@@ -155,10 +155,17 @@ function createJsonEvent(socket, level, message, json) {
|
||||
message
|
||||
});
|
||||
}
|
||||
logger.log("ws-log-event-json", level, socket.user.email, socket.recordid, {
|
||||
wsmessage: message,
|
||||
json
|
||||
});
|
||||
logger.log(
|
||||
"ws-log-event-json",
|
||||
level,
|
||||
socket.user.email,
|
||||
socket.recordid,
|
||||
{
|
||||
wsmessage: message,
|
||||
json
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
if (socket.logEvents && isArray(socket.logEvents)) {
|
||||
socket.logEvents.push({
|
||||
@@ -189,7 +196,8 @@ function createXmlEvent(socket, xml, message, isError = false) {
|
||||
{
|
||||
wsmessage: message,
|
||||
xml
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
if (socket.logEvents && isArray(socket.logEvents)) {
|
||||
@@ -212,7 +220,7 @@ function LogLevelHierarchy(level) {
|
||||
return 4;
|
||||
case "INFO":
|
||||
return 3;
|
||||
case "WARNING":
|
||||
case "WARN":
|
||||
return 2;
|
||||
case "ERROR":
|
||||
return 1;
|
||||
|
||||
2
upload/.gitignore
vendored
Normal file
2
upload/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
Reference in New Issue
Block a user