Merge branch 'master-AIO' into feature/IO-3020-IO-3036-imex-lite-rome-lite
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
<babeledit_project be_version="2.7.1" version="1.2">
|
<babeledit_project version="1.2" be_version="2.7.1">
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
BabelEdit project file
|
BabelEdit project file
|
||||||
@@ -21263,6 +21263,27 @@
|
|||||||
<folder_node>
|
<folder_node>
|
||||||
<name>validation</name>
|
<name>validation</name>
|
||||||
<children>
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>dateRangeExceeded</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>invalidemail</name>
|
<name>invalidemail</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -36484,6 +36505,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>total_repairs_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>
|
<concept_node>
|
||||||
<name>total_sales</name>
|
<name>total_sales</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<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
|
||||||
454
client/package-lock.json
generated
454
client/package-lock.json
generated
@@ -85,6 +85,7 @@
|
|||||||
"web-vitals": "^3.5.2"
|
"web-vitals": "^3.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ant-design/icons": "^5.5.1",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.24.7",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@dotenvx/dotenvx": "^1.14.1",
|
"@dotenvx/dotenvx": "^1.14.1",
|
||||||
@@ -2974,358 +2975,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||||
@@ -5065,6 +4714,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -5249,105 +4899,6 @@
|
|||||||
"@sentry/cli-win32-x64": "2.36.2"
|
"@sentry/cli-win32-x64": "2.36.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/cli-darwin": {
|
|
||||||
"version": "2.36.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.36.2.tgz",
|
|
||||||
"integrity": "sha512-To64Pq+pcmecEr+gFXiqaZy8oKhyLQLXO/SVDdf16CUL2qpuahE3bO5h9kFacMxPPxOWcgc2btF+4gYa1+bQTA==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/cli-linux-arm": {
|
|
||||||
"version": "2.36.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.36.2.tgz",
|
|
||||||
"integrity": "sha512-cRSvOQK97WM0m03k/c+LVAWT042Qz887WP/2Gy64eUi/PfArwb+QZZnsu4FCygxK9jnzgLTo4+ewoJVi17xaLQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux",
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/cli-linux-arm64": {
|
|
||||||
"version": "2.36.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.36.2.tgz",
|
|
||||||
"integrity": "sha512-g+FFmj1oJ2iRMsfs1ORz6THOO6MiAR55K9YxdZUBvqfoHLjSMt7Jst43sbZ3O0u55hnfixSKLNzDaTGaM/jxIQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux",
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/cli-linux-i686": {
|
|
||||||
"version": "2.36.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.36.2.tgz",
|
|
||||||
"integrity": "sha512-rjxTw/CMd0Q7qlOb7gWFiwn3hJIxNkhbn1bOU54xj9CZvQSCvh10l7l4Y9o8znJLl41c5kMXVq8yuYws9A7AGQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x86",
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux",
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/cli-linux-x64": {
|
|
||||||
"version": "2.36.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.36.2.tgz",
|
|
||||||
"integrity": "sha512-cF8IPFTlwiC7JgVvSW4rS99sxb1W1N//iANxuzqaDswUnmJLi0AJy/jES87qE5GRB6ljaPVMvH7Kq0OCp3bvPA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux",
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/cli-win32-i686": {
|
|
||||||
"version": "2.36.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.36.2.tgz",
|
|
||||||
"integrity": "sha512-YDH/Kcd8JAo1Bg4jtSwF8dr7FZZ8QbYLMx8q/5eenHpq6VdOgPENsTvayLW3cAjWLcm44u8Ed/gcEK0z1IxQmQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x86",
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sentry/cli-win32-x64": {
|
"node_modules/@sentry/cli-win32-x64": {
|
||||||
"version": "2.36.2",
|
"version": "2.36.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.36.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.36.2.tgz",
|
||||||
@@ -10262,6 +9813,7 @@
|
|||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
|
|||||||
@@ -121,6 +121,7 @@
|
|||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ant-design/icons": "^5.5.1",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.24.7",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@dotenvx/dotenvx": "^1.14.1",
|
"@dotenvx/dotenvx": "^1.14.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Scripts for firebase and firebase messaging
|
// Scripts for firebase and firebase messaging
|
||||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
|
importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js");
|
||||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
|
importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js");
|
||||||
|
|
||||||
// Initialize the Firebase app in the service worker by passing the generated config
|
// Initialize the Firebase app in the service worker by passing the generated config
|
||||||
let firebaseConfig;
|
let firebaseConfig;
|
||||||
@@ -14,7 +14,7 @@ switch (this.location.hostname) {
|
|||||||
storageBucket: "imex-dev.appspot.com",
|
storageBucket: "imex-dev.appspot.com",
|
||||||
messagingSenderId: "759548147434",
|
messagingSenderId: "759548147434",
|
||||||
appId: "1:759548147434:web:e8239868a48ceb36700993",
|
appId: "1:759548147434:web:e8239868a48ceb36700993",
|
||||||
measurementId: "G-K5XRBVVB4S",
|
measurementId: "G-K5XRBVVB4S"
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case "test.imex.online":
|
case "test.imex.online":
|
||||||
@@ -24,7 +24,7 @@ switch (this.location.hostname) {
|
|||||||
projectId: "imex-test",
|
projectId: "imex-test",
|
||||||
storageBucket: "imex-test.appspot.com",
|
storageBucket: "imex-test.appspot.com",
|
||||||
messagingSenderId: "991923618608",
|
messagingSenderId: "991923618608",
|
||||||
appId: "1:991923618608:web:633437569cdad78299bef5",
|
appId: "1:991923618608:web:633437569cdad78299bef5"
|
||||||
// measurementId: "${config.measurementId}",
|
// measurementId: "${config.measurementId}",
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@@ -38,7 +38,7 @@ switch (this.location.hostname) {
|
|||||||
storageBucket: "imex-prod.appspot.com",
|
storageBucket: "imex-prod.appspot.com",
|
||||||
messagingSenderId: "253497221485",
|
messagingSenderId: "253497221485",
|
||||||
appId: "1:253497221485:web:3c81c483b94db84b227a64",
|
appId: "1:253497221485:web:3c81c483b94db84b227a64",
|
||||||
measurementId: "G-NTWBKG2L0M",
|
measurementId: "G-NTWBKG2L0M"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +49,6 @@ const messaging = firebase.messaging();
|
|||||||
|
|
||||||
messaging.onBackgroundMessage(function (payload) {
|
messaging.onBackgroundMessage(function (payload) {
|
||||||
// Customize notification here
|
// Customize notification here
|
||||||
const channel = new BroadcastChannel("imex-sw-messages");
|
console.log("[firebase-messaging-sw.js] Received background message ", payload);
|
||||||
channel.postMessage(payload);
|
self.registration.showNotification(notificationTitle, notificationOptions);
|
||||||
|
|
||||||
//self.registration.showNotification(notificationTitle, notificationOptions);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,89 +1,50 @@
|
|||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { getToken, onMessage } from "@firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
import { Button, notification, Space } from "antd";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect } from "react";
|
import React, { useContext, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext";
|
||||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||||
import FcmHandler from "../../utils/fcm-handler";
|
|
||||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||||
import "./chat-affix.styles.scss";
|
import "./chat-affix.styles.scss";
|
||||||
|
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
||||||
|
|
||||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||||
|
|
||||||
async function SubscribeToTopic() {
|
async function SubscribeToTopicForFCMNotification() {
|
||||||
try {
|
try {
|
||||||
const r = await axios.post("/notifications/subscribe", {
|
await requestForToken();
|
||||||
|
await axios.post("/notifications/subscribe", {
|
||||||
fcm_tokens: await getToken(messaging, {
|
fcm_tokens: await getToken(messaging, {
|
||||||
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
|
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
|
||||||
}),
|
}),
|
||||||
type: "messaging",
|
type: "messaging",
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
console.log("FCM Topic Subscription", r.data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error attempting to subscribe to messaging topic: ", error);
|
console.log("Error attempting to subscribe to messaging topic: ", error);
|
||||||
notification.open({
|
|
||||||
key: "fcm",
|
|
||||||
type: "warning",
|
|
||||||
message: t("general.errors.fcm"),
|
|
||||||
btn: (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await requestForToken();
|
|
||||||
SubscribeToTopic();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.actions.tryagain")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
const win = window.open(
|
|
||||||
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
win.focus();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.labels.help")}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SubscribeToTopic();
|
SubscribeToTopicForFCMNotification();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [bodyshop]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
//Register WS handlers
|
||||||
function handleMessage(payload) {
|
if (socket && socket.connected) {
|
||||||
FcmHandler({
|
registerMessagingHandlers({ socket, client });
|
||||||
client,
|
|
||||||
payload: (payload && payload.data && payload.data.data) || payload.data
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let stopMessageListener, channel;
|
|
||||||
try {
|
|
||||||
stopMessageListener = onMessage(messaging, handleMessage);
|
|
||||||
channel = new BroadcastChannel("imex-sw-messages");
|
|
||||||
channel.addEventListener("message", handleMessage);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Unable to set event listeners.");
|
|
||||||
}
|
|
||||||
return () => {
|
return () => {
|
||||||
stopMessageListener && stopMessageListener();
|
if (socket && socket.connected) {
|
||||||
channel && channel.removeEventListener("message", handleMessage);
|
unregisterMessagingHandlers({ socket });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [client]);
|
}, [bodyshop, socket, t, client]);
|
||||||
|
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,434 @@
|
|||||||
|
import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
const logLocal = (message, ...args) => {
|
||||||
|
if (import.meta.env.VITE_APP_IS_TEST || !import.meta.env.PROD) {
|
||||||
|
console.log(`==================== ${message} ====================`);
|
||||||
|
console.dir({ ...args });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility function to enrich conversation data
|
||||||
|
const enrichConversation = (conversation, isOutbound) => ({
|
||||||
|
...conversation,
|
||||||
|
updated_at: conversation.updated_at || new Date().toISOString(),
|
||||||
|
unreadcnt: conversation.unreadcnt || 0,
|
||||||
|
archived: conversation.archived || false,
|
||||||
|
label: conversation.label || null,
|
||||||
|
job_conversations: conversation.job_conversations || [],
|
||||||
|
messages_aggregate: conversation.messages_aggregate || {
|
||||||
|
__typename: "messages_aggregate",
|
||||||
|
aggregate: {
|
||||||
|
__typename: "messages_aggregate_fields",
|
||||||
|
count: isOutbound ? 0 : 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
__typename: "conversations"
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerMessagingHandlers = ({ socket, client }) => {
|
||||||
|
if (!(socket && client)) return;
|
||||||
|
|
||||||
|
const handleNewMessageSummary = async (message) => {
|
||||||
|
const { conversationId, newConversation, existingConversation, isoutbound } = message;
|
||||||
|
|
||||||
|
logLocal("handleNewMessageSummary - Start", { message, isNew: !existingConversation });
|
||||||
|
|
||||||
|
const queryVariables = { offset: 0 };
|
||||||
|
|
||||||
|
if (!existingConversation && conversationId) {
|
||||||
|
// Attempt to read from the cache to determine if this is actually a new conversation
|
||||||
|
try {
|
||||||
|
const cachedConversation = client.cache.readFragment({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fragment: gql`
|
||||||
|
fragment ExistingConversationCheck on conversations {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cachedConversation) {
|
||||||
|
logLocal("handleNewMessageSummary - Existing Conversation inferred from cache", {
|
||||||
|
conversationId
|
||||||
|
});
|
||||||
|
return handleNewMessageSummary({
|
||||||
|
...message,
|
||||||
|
existingConversation: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logLocal("handleNewMessageSummary - Cache miss", { conversationId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle new conversation
|
||||||
|
if (!existingConversation && newConversation?.phone_num) {
|
||||||
|
logLocal("handleNewMessageSummary - New Conversation", newConversation);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const queryResults = client.cache.readQuery({
|
||||||
|
query: CONVERSATION_LIST_QUERY,
|
||||||
|
variables: queryVariables
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingConversations = queryResults?.conversations || [];
|
||||||
|
const enrichedConversation = enrichConversation(newConversation, isoutbound);
|
||||||
|
|
||||||
|
if (!existingConversations.some((conv) => conv.id === enrichedConversation.id)) {
|
||||||
|
client.cache.modify({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fields: {
|
||||||
|
conversations(existingConversations = []) {
|
||||||
|
return [enrichedConversation, ...existingConversations];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for new conversation:", error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle existing conversation
|
||||||
|
if (existingConversation) {
|
||||||
|
try {
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
updated_at: () => new Date().toISOString(),
|
||||||
|
archived: () => false,
|
||||||
|
messages_aggregate(cached = { aggregate: { count: 0 } }) {
|
||||||
|
const currentCount = cached.aggregate?.count || 0;
|
||||||
|
if (!isoutbound) {
|
||||||
|
return {
|
||||||
|
__typename: "messages_aggregate",
|
||||||
|
aggregate: {
|
||||||
|
__typename: "messages_aggregate_fields",
|
||||||
|
count: currentCount + 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for existing conversation:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logLocal("New Conversation Summary finished without work", { message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewMessageDetailed = (message) => {
|
||||||
|
const { conversationId, newMessage } = message;
|
||||||
|
|
||||||
|
logLocal("handleNewMessageDetailed - Start", message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if the conversation exists in the cache
|
||||||
|
const queryResults = client.cache.readQuery({
|
||||||
|
query: GET_CONVERSATION_DETAILS,
|
||||||
|
variables: { conversationId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!queryResults?.conversations_by_pk) {
|
||||||
|
console.warn("Conversation not found in cache:", { conversationId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the new message to the conversation's message list using cache.modify
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
messages(existingMessages = []) {
|
||||||
|
return [...existingMessages, newMessage];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logLocal("handleNewMessageDetailed - Message appended successfully", {
|
||||||
|
conversationId,
|
||||||
|
newMessage
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating conversation messages in cache:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageChanged = (message) => {
|
||||||
|
if (!message) {
|
||||||
|
logLocal("handleMessageChanged - No message provided", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logLocal("handleMessageChanged - Start", message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: message.conversationid }),
|
||||||
|
fields: {
|
||||||
|
messages(existingMessages = [], { readField }) {
|
||||||
|
return existingMessages.map((messageRef) => {
|
||||||
|
// Check if this is the message to update
|
||||||
|
if (readField("id", messageRef) === message.id) {
|
||||||
|
const currentStatus = readField("status", messageRef);
|
||||||
|
|
||||||
|
// Handle known types of message changes
|
||||||
|
switch (message.type) {
|
||||||
|
case "status-changed":
|
||||||
|
// Prevent overwriting if the current status is already "delivered"
|
||||||
|
if (currentStatus === "delivered") {
|
||||||
|
logLocal("handleMessageChanged - Status already delivered, skipping update", {
|
||||||
|
messageId: message.id
|
||||||
|
});
|
||||||
|
return messageRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the status field
|
||||||
|
return {
|
||||||
|
...messageRef,
|
||||||
|
status: message.status
|
||||||
|
};
|
||||||
|
|
||||||
|
case "text-updated":
|
||||||
|
// Handle changes to the message text
|
||||||
|
return {
|
||||||
|
...messageRef,
|
||||||
|
text: message.text
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add cases for other known message types as needed
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Log a warning for unhandled message types
|
||||||
|
logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
|
||||||
|
return messageRef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageRef; // Keep other messages unchanged
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logLocal("handleMessageChanged - Message updated successfully", {
|
||||||
|
messageId: message.id,
|
||||||
|
type: message.type
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("handleMessageChanged - Error modifying cache:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConversationChanged = async (data) => {
|
||||||
|
if (!data) {
|
||||||
|
logLocal("handleConversationChanged - No data provided", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { conversationId, type, job_conversations, messageIds, ...fields } = data;
|
||||||
|
logLocal("handleConversationChanged - Start", data);
|
||||||
|
|
||||||
|
const updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const updateConversationList = (newConversation) => {
|
||||||
|
try {
|
||||||
|
const existingList = client.cache.readQuery({
|
||||||
|
query: CONVERSATION_LIST_QUERY,
|
||||||
|
variables: { offset: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedList = existingList?.conversations
|
||||||
|
? [
|
||||||
|
newConversation,
|
||||||
|
...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates
|
||||||
|
]
|
||||||
|
: [newConversation];
|
||||||
|
|
||||||
|
client.cache.writeQuery({
|
||||||
|
query: CONVERSATION_LIST_QUERY,
|
||||||
|
variables: { offset: 0 },
|
||||||
|
data: {
|
||||||
|
conversations: updatedList
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logLocal("handleConversationChanged - Conversation list updated successfully", newConversation);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating conversation list in the cache:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle specific types
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case "conversation-marked-read":
|
||||||
|
if (conversationId && messageIds?.length > 0) {
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
messages(existingMessages = [], { readField }) {
|
||||||
|
return existingMessages.map((message) => {
|
||||||
|
if (messageIds.includes(readField("id", message))) {
|
||||||
|
return { ...message, read: true };
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
messages_aggregate: () => ({
|
||||||
|
__typename: "messages_aggregate",
|
||||||
|
aggregate: { __typename: "messages_aggregate_fields", count: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "conversation-created":
|
||||||
|
updateConversationList({ ...fields, job_conversations, updated_at: updatedAt });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "conversation-unarchived":
|
||||||
|
case "conversation-archived":
|
||||||
|
// Would like to someday figure out how to get this working without refetch queries,
|
||||||
|
// But I have but a solid 4 hours into it, and there are just too many weird occurrences
|
||||||
|
try {
|
||||||
|
const listQueryVariables = { offset: 0 };
|
||||||
|
const detailsQueryVariables = { conversationId };
|
||||||
|
|
||||||
|
// Check if conversation details exist in the cache
|
||||||
|
const detailsExist = !!client.cache.readQuery({
|
||||||
|
query: GET_CONVERSATION_DETAILS,
|
||||||
|
variables: detailsQueryVariables
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refetch conversation list
|
||||||
|
await client.refetchQueries({
|
||||||
|
include: [CONVERSATION_LIST_QUERY, ...(detailsExist ? [GET_CONVERSATION_DETAILS] : [])],
|
||||||
|
variables: [
|
||||||
|
{ query: CONVERSATION_LIST_QUERY, variables: listQueryVariables },
|
||||||
|
...(detailsExist
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
query: GET_CONVERSATION_DETAILS,
|
||||||
|
variables: detailsQueryVariables
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
logLocal("handleConversationChanged - Refetched queries after state change", {
|
||||||
|
conversationId,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refetching queries after conversation state change:", error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "tag-added":
|
||||||
|
// Ensure `job_conversations` is properly formatted
|
||||||
|
const formattedJobConversations = job_conversations.map((jc) => ({
|
||||||
|
__typename: "job_conversations",
|
||||||
|
jobid: jc.jobid || jc.job?.id,
|
||||||
|
conversationid: conversationId,
|
||||||
|
job: jc.job || {
|
||||||
|
__typename: "jobs",
|
||||||
|
id: data.selectedJob.id,
|
||||||
|
ro_number: data.selectedJob.ro_number,
|
||||||
|
ownr_co_nm: data.selectedJob.ownr_co_nm,
|
||||||
|
ownr_fn: data.selectedJob.ownr_fn,
|
||||||
|
ownr_ln: data.selectedJob.ownr_ln
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
job_conversations: (existing = []) => {
|
||||||
|
// Ensure no duplicates based on both `conversationid` and `jobid`
|
||||||
|
const existingLinks = new Set(
|
||||||
|
existing.map((jc) => {
|
||||||
|
const jobId = client.cache.readFragment({
|
||||||
|
id: client.cache.identify(jc),
|
||||||
|
fragment: gql`
|
||||||
|
fragment JobConversationLinkAdded on job_conversations {
|
||||||
|
jobid
|
||||||
|
conversationid
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})?.jobid;
|
||||||
|
return `${jobId}:${conversationId}`; // Unique identifier for a job-conversation link
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const newItems = formattedJobConversations.filter((jc) => {
|
||||||
|
const uniqueLink = `${jc.jobid}:${jc.conversationid}`;
|
||||||
|
return !existingLinks.has(uniqueLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...existing, ...newItems];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "tag-removed":
|
||||||
|
try {
|
||||||
|
const conversationCacheId = client.cache.identify({ __typename: "conversations", id: conversationId });
|
||||||
|
|
||||||
|
// Evict the specific cache entry for job_conversations
|
||||||
|
client.cache.evict({
|
||||||
|
id: conversationCacheId,
|
||||||
|
fieldName: "job_conversations"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Garbage collect evicted entries
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
|
logLocal("handleConversationChanged - tag removed - Refetched conversation list after state change", {
|
||||||
|
conversationId,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refetching queries after conversation state change: (Tag Removed)", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logLocal("handleConversationChanged - Unhandled type", { type });
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(fields).map(([key, value]) => [key, (cached) => (value !== undefined ? value : cached)])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling conversation changes:", { type, error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("new-message-summary", handleNewMessageSummary);
|
||||||
|
socket.on("new-message-detailed", handleNewMessageDetailed);
|
||||||
|
socket.on("message-changed", handleMessageChanged);
|
||||||
|
socket.on("conversation-changed", handleConversationChanged);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unregisterMessagingHandlers = ({ socket }) => {
|
||||||
|
if (!socket) return;
|
||||||
|
socket.off("new-message-summary");
|
||||||
|
socket.off("new-message-detailed");
|
||||||
|
socket.off("message-changed");
|
||||||
|
socket.off("conversation-changed");
|
||||||
|
};
|
||||||
@@ -1,27 +1,49 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button } from "antd";
|
import { Button } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatArchiveButton({ conversation }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatArchiveButton({ conversation, bodyshop }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const handleToggleArchive = async () => {
|
const handleToggleArchive = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
await updateConversation({
|
const updatedConversation = await updateConversation({
|
||||||
variables: { id: conversation.id, archived: !conversation.archived },
|
variables: { id: conversation.id, archived: !conversation.archived }
|
||||||
refetchQueries: ["CONVERSATION_LIST_QUERY"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
type: "conversation-archived",
|
||||||
|
conversationId: conversation.id,
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
archived: updatedConversation.data.update_conversations_by_pk.archived
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleToggleArchive} loading={loading} type="primary">
|
<Button onClick={handleToggleArchive} loading={loading} className="archive-button" type="primary">
|
||||||
{conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
|
{conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatArchiveButton);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Badge, Card, List, Space, Tag } from "antd";
|
import { Badge, Card, List, Space, Tag } from "antd";
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList } from "react-virtualized";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
@@ -19,19 +19,26 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||||
});
|
});
|
||||||
|
|
||||||
function ChatConversationListComponent({
|
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
||||||
conversationList,
|
// That comma is there for a reason, do not remove it
|
||||||
selectedConversation,
|
const [, forceUpdate] = useState(false);
|
||||||
setSelectedConversation,
|
|
||||||
loadMoreConversations
|
|
||||||
}) {
|
|
||||||
const cache = new CellMeasurerCache({
|
|
||||||
fixedWidth: true,
|
|
||||||
defaultHeight: 60
|
|
||||||
});
|
|
||||||
|
|
||||||
const rowRenderer = ({ index, key, style, parent }) => {
|
// Re-render every minute
|
||||||
const item = conversationList[index];
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
|
||||||
|
}, 60000); // 1 minute in milliseconds
|
||||||
|
|
||||||
|
return () => clearInterval(interval); // Cleanup on unmount
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Memoize the sorted conversation list
|
||||||
|
const sortedConversationList = React.useMemo(() => {
|
||||||
|
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
||||||
|
}, [conversationList]);
|
||||||
|
|
||||||
|
const renderConversation = (index) => {
|
||||||
|
const item = sortedConversationList[index];
|
||||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
const cardContentLeft =
|
const cardContentLeft =
|
||||||
item.job_conversations.length > 0
|
item.job_conversations.length > 0
|
||||||
@@ -52,7 +59,8 @@ function ChatConversationListComponent({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0} />;
|
|
||||||
|
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count} />;
|
||||||
|
|
||||||
const getCardStyle = () =>
|
const getCardStyle = () =>
|
||||||
item.id === selectedConversation
|
item.id === selectedConversation
|
||||||
@@ -60,40 +68,26 @@ function ChatConversationListComponent({
|
|||||||
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellMeasurer key={key} cache={cache} parent={parent} columnIndex={0} rowIndex={index}>
|
<List.Item
|
||||||
<List.Item
|
key={item.id}
|
||||||
onClick={() => setSelectedConversation(item.id)}
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
style={style}
|
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||||
className={`chat-list-item
|
>
|
||||||
${item.id === selectedConversation ? "chat-list-selected-conversation" : null}`}
|
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||||
>
|
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
</Card>
|
||||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
</List.Item>
|
||||||
</Card>
|
|
||||||
</List.Item>
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-list-container">
|
<div className="chat-list-container">
|
||||||
<AutoSizer>
|
<Virtuoso
|
||||||
{({ height, width }) => (
|
data={sortedConversationList}
|
||||||
<VirtualizedList
|
itemContent={(index) => renderConversation(index)}
|
||||||
height={height}
|
style={{ height: "100%", width: "100%" }}
|
||||||
width={width}
|
/>
|
||||||
rowCount={conversationList.length}
|
|
||||||
rowHeight={cache.rowHeight}
|
|
||||||
rowRenderer={rowRenderer}
|
|
||||||
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
|
|
||||||
if (scrollTop + clientHeight === scrollHeight) {
|
|
||||||
loadMoreConversations();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.chat-list-container {
|
.chat-list-container {
|
||||||
overflow: hidden;
|
height: 100%; /* Ensure it takes up the full available height */
|
||||||
height: 100%;
|
|
||||||
border: 1px solid gainsboro;
|
border: 1px solid gainsboro;
|
||||||
|
overflow: auto; /* Allow scrolling for the Virtuoso component */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
@@ -14,3 +14,24 @@
|
|||||||
color: #ff7a00;
|
color: #ff7a00;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Virtuoso item container adjustments */
|
||||||
|
.chat-list-container > div {
|
||||||
|
height: 100%; /* Ensure Virtuoso takes full height */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add spacing and better alignment for items */
|
||||||
|
.chat-list-item {
|
||||||
|
padding: 0.5rem 0; /* Add spacing between list items */
|
||||||
|
|
||||||
|
.ant-card {
|
||||||
|
border-radius: 8px; /* Slight rounding for card edges */
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* Subtle shadow for better definition */
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .ant-card {
|
||||||
|
border-color: #ff7a00; /* Highlight border on hover */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatConversationTitleTags({ jobConversations }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatConversationTitleTags({ jobConversations, bodyshop }) {
|
||||||
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const handleRemoveTag = (jobId) => {
|
const handleRemoveTag = async (jobId) => {
|
||||||
const convId = jobConversations[0].conversationid;
|
const convId = jobConversations[0].conversationid;
|
||||||
if (!!convId) {
|
if (!!convId) {
|
||||||
removeJobConversation({
|
await removeJobConversation({
|
||||||
variables: {
|
variables: {
|
||||||
conversationId: convId,
|
conversationId: convId,
|
||||||
jobId: jobId
|
jobId: jobId
|
||||||
@@ -28,6 +39,17 @@ export default function ChatConversationTitleTags({ jobConversations }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
// Emit the `conversation-modified` event
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
conversationId: convId,
|
||||||
|
type: "tag-removed",
|
||||||
|
jobId: jobId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logImEXEvent("messaging_remove_job_tag", {
|
logImEXEvent("messaging_remove_job_tag", {
|
||||||
conversationId: convId,
|
conversationId: convId,
|
||||||
jobId: jobId
|
jobId: jobId
|
||||||
@@ -54,3 +76,5 @@ export default function ChatConversationTitleTags({ jobConversations }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationTitleTags);
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conv
|
|||||||
import ChatLabelComponent from "../chat-label/chat-label.component";
|
import ChatLabelComponent from "../chat-label/chat-label.component";
|
||||||
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
||||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatConversationTitle({ conversation }) {
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatConversationTitle({ conversation }) {
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space className="chat-title" wrap>
|
||||||
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
|
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
|
||||||
<ChatLabelComponent conversation={conversation} />
|
<ChatLabelComponent conversation={conversation} />
|
||||||
<ChatPrintButton conversation={conversation} />
|
<ChatPrintButton conversation={conversation} />
|
||||||
@@ -19,3 +25,5 @@ export default function ChatConversationTitle({ conversation }) {
|
|||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationTitle);
|
||||||
|
|||||||
@@ -5,10 +5,26 @@ import ChatMessageListComponent from "../chat-messages-list/chat-message-list.co
|
|||||||
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
||||||
import "./chat-conversation.styles.scss";
|
import "./chat-conversation.styles.scss";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatConversationComponent({
|
||||||
|
subState,
|
||||||
|
conversation,
|
||||||
|
messages,
|
||||||
|
handleMarkConversationAsRead,
|
||||||
|
bodyshop
|
||||||
|
}) {
|
||||||
const [loading, error] = subState;
|
const [loading, error] = subState;
|
||||||
|
|
||||||
|
if (conversation?.archived) return null;
|
||||||
if (loading) return <LoadingSkeleton />;
|
if (loading) return <LoadingSkeleton />;
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
@@ -18,9 +34,11 @@ export default function ChatConversationComponent({ subState, conversation, mess
|
|||||||
onMouseDown={handleMarkConversationAsRead}
|
onMouseDown={handleMarkConversationAsRead}
|
||||||
onKeyDown={handleMarkConversationAsRead}
|
onKeyDown={handleMarkConversationAsRead}
|
||||||
>
|
>
|
||||||
<ChatConversationTitle conversation={conversation} />
|
<ChatConversationTitle conversation={conversation} bodyshop={bodyshop} />
|
||||||
<ChatMessageListComponent messages={messages} />
|
<ChatMessageListComponent messages={messages} />
|
||||||
<ChatSendMessage conversation={conversation} />
|
<ChatSendMessage conversation={conversation} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationComponent);
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import { useMutation, useQuery, useSubscription } from "@apollo/client";
|
import { gql, useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||||
import React, { useState } from "react";
|
import axios from "axios";
|
||||||
|
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
import SocketContext from "../../contexts/SocketIO/socketContext";
|
||||||
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
import { GET_CONVERSATION_DETAILS, CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import ChatConversationComponent from "./chat-conversation.component";
|
|
||||||
import axios from "axios";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import ChatConversationComponent from "./chat-conversation.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(ChatConversationContainer);
|
function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||||
|
const client = useApolloClient();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||||
|
|
||||||
export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
// Fetch conversation details
|
||||||
const {
|
const {
|
||||||
loading: convoLoading,
|
loading: convoLoading,
|
||||||
error: convoError,
|
error: convoError,
|
||||||
@@ -27,55 +30,145 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
// Subscription for conversation updates
|
||||||
variables: { conversationId: selectedConversation }
|
useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
||||||
});
|
skip: socket?.connected,
|
||||||
|
|
||||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
|
||||||
|
|
||||||
const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, {
|
|
||||||
variables: { conversationId: selectedConversation },
|
variables: { conversationId: selectedConversation },
|
||||||
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
onData: ({ data: subscriptionResult, client }) => {
|
||||||
update(cache) {
|
// Extract the messages array from the result
|
||||||
cache.modify({
|
const messages = subscriptionResult?.data?.messages;
|
||||||
id: cache.identify({
|
if (!messages || messages.length === 0) {
|
||||||
__typename: "conversations",
|
console.warn("No messages found in subscription result.");
|
||||||
id: selectedConversation
|
return;
|
||||||
}),
|
}
|
||||||
fields: {
|
|
||||||
messages_aggregate(cached) {
|
messages.forEach((message) => {
|
||||||
return { aggregate: { count: 0 } };
|
const messageRef = client.cache.identify(message);
|
||||||
|
// Write the new message to the cache
|
||||||
|
client.cache.writeFragment({
|
||||||
|
id: messageRef,
|
||||||
|
fragment: gql`
|
||||||
|
fragment NewMessage on messages {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
text
|
||||||
|
isoutbound
|
||||||
|
image
|
||||||
|
image_path
|
||||||
|
userid
|
||||||
|
created_at
|
||||||
|
read
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
data: message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the conversation cache to include the new message
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: selectedConversation }),
|
||||||
|
fields: {
|
||||||
|
messages(existingMessages = []) {
|
||||||
|
const alreadyExists = existingMessages.some((msg) => msg.__ref === messageRef);
|
||||||
|
if (alreadyExists) return existingMessages;
|
||||||
|
return [...existingMessages, { __ref: messageRef }];
|
||||||
|
},
|
||||||
|
updated_at() {
|
||||||
|
return message.created_at;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const unreadCount =
|
const updateCacheWithReadMessages = useCallback(
|
||||||
data &&
|
(conversationId, messageIds) => {
|
||||||
data.messages &&
|
if (!conversationId || !messageIds?.length) return;
|
||||||
data.messages.reduce((acc, val) => {
|
|
||||||
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const handleMarkConversationAsRead = async () => {
|
messageIds.forEach((messageId) => {
|
||||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
client.cache.modify({
|
||||||
setMarkingAsReadInProgress(true);
|
id: client.cache.identify({ __typename: "messages", id: messageId }),
|
||||||
await markConversationRead({});
|
fields: {
|
||||||
await axios.post("/sms/markConversationRead", {
|
read: () => true
|
||||||
conversationid: selectedConversation,
|
}
|
||||||
imexshopid: bodyshop.imexshopid
|
});
|
||||||
});
|
});
|
||||||
setMarkingAsReadInProgress(false);
|
},
|
||||||
|
[client.cache]
|
||||||
|
);
|
||||||
|
|
||||||
|
// WebSocket event handlers
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket?.connected) return;
|
||||||
|
|
||||||
|
const handleConversationChange = (data) => {
|
||||||
|
if (data.type === "conversation-marked-read") {
|
||||||
|
const { conversationId, messageIds } = data;
|
||||||
|
updateCacheWithReadMessages(conversationId, messageIds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("conversation-changed", handleConversationChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("conversation-changed", handleConversationChange);
|
||||||
|
};
|
||||||
|
}, [socket, updateCacheWithReadMessages]);
|
||||||
|
|
||||||
|
// Join and leave conversation via WebSocket
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket?.connected || !selectedConversation || !bodyshop?.id) return;
|
||||||
|
|
||||||
|
socket.emit("join-bodyshop-conversation", {
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
conversationId: selectedConversation
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.emit("leave-bodyshop-conversation", {
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
conversationId: selectedConversation
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [socket, bodyshop, selectedConversation]);
|
||||||
|
|
||||||
|
// Mark conversation as read
|
||||||
|
const handleMarkConversationAsRead = async () => {
|
||||||
|
if (!convoData || markingAsReadInProgress) return;
|
||||||
|
|
||||||
|
const conversation = convoData.conversations_by_pk;
|
||||||
|
if (!conversation) return;
|
||||||
|
|
||||||
|
const unreadMessageIds = conversation.messages
|
||||||
|
?.filter((message) => !message.read && !message.isoutbound)
|
||||||
|
.map((message) => message.id);
|
||||||
|
|
||||||
|
if (unreadMessageIds?.length > 0) {
|
||||||
|
setMarkingAsReadInProgress(true);
|
||||||
|
try {
|
||||||
|
await axios.post("/sms/markConversationRead", {
|
||||||
|
conversation,
|
||||||
|
imexshopid: bodyshop?.imexshopid,
|
||||||
|
bodyshopid: bodyshop?.id
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCacheWithReadMessages(selectedConversation, unreadMessageIds);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error marking conversation as read:", error.message);
|
||||||
|
} finally {
|
||||||
|
setMarkingAsReadInProgress(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatConversationComponent
|
<ChatConversationComponent
|
||||||
subState={[loading || convoLoading, error || convoError]}
|
subState={[convoLoading, convoError]}
|
||||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
conversation={convoData?.conversations_by_pk || {}}
|
||||||
messages={data ? data.messages : []}
|
messages={convoData?.conversations_by_pk?.messages || []}
|
||||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ChatConversationContainer);
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
import { PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Input, notification, Spin, Tag, Tooltip } from "antd";
|
import { Input, notification, Spin, Tag, Tooltip } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatLabel({ conversation }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
|
||||||
|
export function ChatLabel({ conversation, bodyshop }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [value, setValue] = useState(conversation.label);
|
const [value, setValue] = useState(conversation.label);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
||||||
@@ -26,6 +37,14 @@ export default function ChatLabel({ conversation }) {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (socket) {
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
type: "label-updated",
|
||||||
|
conversationId: conversation.id,
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
label: value
|
||||||
|
});
|
||||||
|
}
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -57,3 +76,5 @@ export default function ChatLabel({ conversation }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatLabel);
|
||||||
|
|||||||
@@ -1,106 +1,87 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Tooltip } from "antd";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import i18n from "i18next";
|
import { renderMessage } from "./renderMessage";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
|
||||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
|
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
|
||||||
import "./chat-message-list.styles.scss";
|
import "./chat-message-list.styles.scss";
|
||||||
|
|
||||||
export default function ChatMessageListComponent({ messages }) {
|
export default function ChatMessageListComponent({ messages }) {
|
||||||
const virtualizedListRef = useRef(null);
|
const virtuosoRef = useRef(null);
|
||||||
|
const [atBottom, setAtBottom] = useState(true);
|
||||||
|
const loadedImagesRef = useRef(0);
|
||||||
|
|
||||||
const _cache = new CellMeasurerCache({
|
const handleScrollStateChange = (isAtBottom) => {
|
||||||
fixedWidth: true,
|
setAtBottom(isAtBottom);
|
||||||
// minHeight: 50,
|
|
||||||
defaultHeight: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
const scrollToBottom = (renderedrows) => {
|
|
||||||
//console.log("Scrolling to", messages.length);
|
|
||||||
// !!virtualizedListRef.current &&
|
|
||||||
// virtualizedListRef.current.scrollToRow(messages.length);
|
|
||||||
// Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
|
|
||||||
//Scrolling does not work on this version of React.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(scrollToBottom, [messages]);
|
const resetImageLoadState = () => {
|
||||||
|
loadedImagesRef.current = 0;
|
||||||
const _rowRenderer = ({ index, key, parent, style }) => {
|
|
||||||
return (
|
|
||||||
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
|
||||||
{({ measure, registerChild }) => (
|
|
||||||
<div
|
|
||||||
ref={registerChild}
|
|
||||||
onLoad={measure}
|
|
||||||
style={style}
|
|
||||||
className={`${messages[index].isoutbound ? "mine messages" : "yours messages"}`}
|
|
||||||
>
|
|
||||||
<div className="message msgmargin">
|
|
||||||
{MessageRender(messages[index])}
|
|
||||||
{StatusRender(messages[index].status)}
|
|
||||||
</div>
|
|
||||||
{messages[index].isoutbound && (
|
|
||||||
<div style={{ fontSize: 10 }}>
|
|
||||||
{i18n.t("messaging.labels.sentby", {
|
|
||||||
by: messages[index].userid,
|
|
||||||
time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a")
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const preloadImages = useCallback((imagePaths, onComplete) => {
|
||||||
|
resetImageLoadState();
|
||||||
|
|
||||||
|
if (imagePaths.length === 0) {
|
||||||
|
onComplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePaths.forEach((url) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = url;
|
||||||
|
img.onload = img.onerror = () => {
|
||||||
|
loadedImagesRef.current += 1;
|
||||||
|
if (loadedImagesRef.current === imagePaths.length) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Ensure all images are loaded on initial render
|
||||||
|
useEffect(() => {
|
||||||
|
const imagePaths = messages
|
||||||
|
.filter((message) => message.image && message.image_path?.length > 0)
|
||||||
|
.flatMap((message) => message.image_path);
|
||||||
|
|
||||||
|
preloadImages(imagePaths, () => {
|
||||||
|
if (virtuosoRef.current) {
|
||||||
|
virtuosoRef.current.scrollToIndex({
|
||||||
|
index: messages.length - 1,
|
||||||
|
align: "end",
|
||||||
|
behavior: "auto"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [messages, preloadImages]);
|
||||||
|
|
||||||
|
// Handle scrolling when new messages are added
|
||||||
|
useEffect(() => {
|
||||||
|
if (!atBottom) return;
|
||||||
|
|
||||||
|
const latestMessage = messages[messages.length - 1];
|
||||||
|
const imagePaths = latestMessage?.image_path || [];
|
||||||
|
|
||||||
|
preloadImages(imagePaths, () => {
|
||||||
|
if (virtuosoRef.current) {
|
||||||
|
virtuosoRef.current.scrollToIndex({
|
||||||
|
index: messages.length - 1,
|
||||||
|
align: "end",
|
||||||
|
behavior: "smooth"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [messages, atBottom, preloadImages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat">
|
<div className="chat">
|
||||||
<AutoSizer>
|
<Virtuoso
|
||||||
{({ height, width }) => (
|
ref={virtuosoRef}
|
||||||
<List
|
data={messages}
|
||||||
ref={virtualizedListRef}
|
overscan={!!messages.reduce((acc, message) => acc + (message.image_path?.length || 0), 0) ? messages.length : 0}
|
||||||
width={width}
|
itemContent={(index) => renderMessage(messages, index)}
|
||||||
height={height}
|
followOutput={(isAtBottom) => handleScrollStateChange(isAtBottom)}
|
||||||
rowHeight={_cache.rowHeight}
|
initialTopMostItemIndex={messages.length - 1}
|
||||||
rowRenderer={_rowRenderer}
|
style={{ height: "100%", width: "100%" }}
|
||||||
rowCount={messages.length}
|
/>
|
||||||
overscanRowCount={10}
|
|
||||||
estimatedRowSize={150}
|
|
||||||
scrollToIndex={messages.length}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageRender = (message) => {
|
|
||||||
return (
|
|
||||||
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
|
||||||
<div>
|
|
||||||
{message.image_path &&
|
|
||||||
message.image_path.map((i, idx) => (
|
|
||||||
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
|
||||||
<a href={i} target="__blank">
|
|
||||||
<img alt="Received" className="message-img" src={i} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div>{message.text}</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusRender = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case "sent":
|
|
||||||
return <Icon component={MdDone} className="message-icon" />;
|
|
||||||
case "delivered":
|
|
||||||
return <Icon component={MdDoneAll} className="message-icon" />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,119 +1,131 @@
|
|||||||
.message-icon {
|
|
||||||
//position: absolute;
|
|
||||||
// bottom: 0rem;
|
|
||||||
color: whitesmoke;
|
|
||||||
border: #000000;
|
|
||||||
position: absolute;
|
|
||||||
margin: 0 0.1rem;
|
|
||||||
bottom: 0.1rem;
|
|
||||||
right: 0.3rem;
|
|
||||||
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat {
|
.chat {
|
||||||
flex: 1;
|
|
||||||
//width: 300px;
|
|
||||||
//border: solid 1px #eee;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0.8rem 0rem;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.archive-button {
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.chat-title {
|
||||||
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
//margin-top: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 0.5rem; // Prevent edge clipping
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
position: relative;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 0.25rem 0.8rem;
|
padding: 0.25rem 0.8rem;
|
||||||
//margin-top: 5px;
|
word-wrap: break-word;
|
||||||
// margin-bottom: 5px;
|
|
||||||
//display: inline-block;
|
|
||||||
|
|
||||||
.message-img {
|
&-img {
|
||||||
max-width: 10rem;
|
max-width: 10rem;
|
||||||
max-height: 10rem;
|
max-height: 10rem;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
margin: 0.2rem;
|
margin: 0.2rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.chat-send-message-button{
|
||||||
.yours {
|
margin: 0.3rem;
|
||||||
align-items: flex-start;
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
.message-icon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.1rem;
|
||||||
|
right: 0.3rem;
|
||||||
|
margin: 0 0.1rem;
|
||||||
|
color: whitesmoke;
|
||||||
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msgmargin {
|
.msgmargin {
|
||||||
margin-top: 0.1rem;
|
margin: 0.1rem 0;
|
||||||
margin-bottom: 0.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.yours .message {
|
.yours,
|
||||||
margin-right: 20%;
|
.mine {
|
||||||
background-color: #eee;
|
display: flex;
|
||||||
position: relative;
|
flex-direction: column;
|
||||||
|
|
||||||
|
.message {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:last-child:before,
|
||||||
|
&:last-child:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child:after {
|
||||||
|
width: 10px;
|
||||||
|
background: white;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.yours .message.last:before {
|
/* "Yours" (incoming) message styles */
|
||||||
content: "";
|
.yours {
|
||||||
position: absolute;
|
align-items: flex-start;
|
||||||
z-index: 0;
|
|
||||||
bottom: 0;
|
.message {
|
||||||
left: -7px;
|
margin-right: 20%;
|
||||||
height: 20px;
|
background-color: #eee;
|
||||||
width: 20px;
|
|
||||||
background: #eee;
|
&:last-child:before {
|
||||||
border-bottom-right-radius: 15px;
|
left: -7px;
|
||||||
}
|
background: #eee;
|
||||||
|
border-bottom-right-radius: 15px;
|
||||||
.yours .message.last:after {
|
}
|
||||||
content: "";
|
|
||||||
position: absolute;
|
&:last-child:after {
|
||||||
z-index: 1;
|
left: -10px;
|
||||||
bottom: 0;
|
border-bottom-right-radius: 10px;
|
||||||
left: -10px;
|
}
|
||||||
width: 10px;
|
}
|
||||||
height: 20px;
|
|
||||||
background: white;
|
|
||||||
border-bottom-right-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* "Mine" (outgoing) message styles */
|
||||||
.mine {
|
.mine {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: white;
|
||||||
|
margin-left: 25%;
|
||||||
|
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||||
|
padding-bottom: 0.6rem;
|
||||||
|
|
||||||
|
&:last-child:before {
|
||||||
|
right: -8px;
|
||||||
|
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||||
|
border-bottom-left-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child:after {
|
||||||
|
right: -10px;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine .message {
|
.virtuoso-container {
|
||||||
color: white;
|
flex: 1;
|
||||||
margin-left: 25%;
|
overflow: auto;
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mine .message.last:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
z-index: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: -8px;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
border-bottom-left-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mine .message.last:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
bottom: 0;
|
|
||||||
right: -10px;
|
|
||||||
width: 10px;
|
|
||||||
height: 20px;
|
|
||||||
background: white;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
client/src/components/chat-messages-list/renderMessage.jsx
Normal file
52
client/src/components/chat-messages-list/renderMessage.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import { Tooltip } from "antd";
|
||||||
|
import i18n from "i18next";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
import { MdDone, MdDoneAll } from "react-icons/md";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
|
export const renderMessage = (messages, index) => {
|
||||||
|
const message = messages[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
|
||||||
|
<div className="message msgmargin">
|
||||||
|
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
||||||
|
<div>
|
||||||
|
{/* Render images if available */}
|
||||||
|
{message.image && message.image_path?.length > 0 && (
|
||||||
|
<div className="message-images">
|
||||||
|
{message.image_path.map((url, idx) => (
|
||||||
|
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<img alt="Received" className="message-img" src={url} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Render text if available */}
|
||||||
|
{message.text && <div>{message.text}</div>}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Message status icons */}
|
||||||
|
{message.status && (message.status === "sent" || message.status === "delivered") && (
|
||||||
|
<div className="message-status">
|
||||||
|
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Outbound message metadata */}
|
||||||
|
{message.isoutbound && (
|
||||||
|
<div style={{ fontSize: 10 }}>
|
||||||
|
{i18n.t("messaging.labels.sentby", {
|
||||||
|
by: message.userid,
|
||||||
|
time: dayjs(message.created_at).format("MM/DD/YYYY @ hh:mm a")
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { PlusCircleFilled } from "@ant-design/icons";
|
import { PlusCircleFilled } from "@ant-design/icons";
|
||||||
import { Button, Form, Popover } from "antd";
|
import { Button, Form, Popover } from "antd";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -17,8 +18,10 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export function ChatNewConversation({ openChatByPhone }) {
|
export function ChatNewConversation({ openChatByPhone }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = (values) => {
|
||||||
openChatByPhone({ phone_num: values.phoneNumber });
|
openChatByPhone({ phone_num: values.phoneNumber, socket });
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
@@ -9,6 +9,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -21,6 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
|
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
if (!phone) return <></>;
|
if (!phone) return <></>;
|
||||||
|
|
||||||
if (!bodyshop.messagingservicesid) return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
if (!bodyshop.messagingservicesid) return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
||||||
@@ -33,7 +36,7 @@ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobi
|
|||||||
const p = parsePhoneNumber(phone, "CA");
|
const p = parsePhoneNumber(phone, "CA");
|
||||||
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
||||||
if (p && p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid, socket });
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({ message: t("messaging.error.invalidphone") });
|
notification["error"]({ message: t("messaging.error.invalidphone") });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
import { useApolloClient, useLazyQuery, useQuery } from "@apollo/client";
|
||||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -13,61 +13,102 @@ import ChatConversationContainer from "../chat-conversation/chat-conversation.co
|
|||||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import "./chat-popup.styles.scss";
|
import "./chat-popup.styles.scss";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
chatVisible: selectChatVisible
|
chatVisible: selectChatVisible
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleChatVisible: () => dispatch(toggleChatVisible())
|
toggleChatVisible: () => dispatch(toggleChatVisible())
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
|
export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [pollInterval, setpollInterval] = useState(0);
|
const [pollInterval, setPollInterval] = useState(0);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
const client = useApolloClient(); // Apollo Client instance for cache operations
|
||||||
|
|
||||||
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
// Lazy query for conversations
|
||||||
fetchPolicy: "network-only",
|
const [getConversations, { loading, data, refetch }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||||
nextFetchPolicy: "network-only",
|
|
||||||
...(pollInterval > 0 ? { pollInterval } : {})
|
|
||||||
});
|
|
||||||
|
|
||||||
const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: !chatVisible,
|
skip: !chatVisible,
|
||||||
...(pollInterval > 0 ? { pollInterval } : {})
|
...(pollInterval > 0 ? { pollInterval } : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
// Query for unread count when chat is not visible
|
||||||
|
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only",
|
||||||
|
skip: chatVisible, // Skip when chat is visible
|
||||||
|
...(pollInterval > 0 ? { pollInterval } : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Socket connection status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fcmToken) {
|
const handleSocketStatus = () => {
|
||||||
setpollInterval(0);
|
if (socket?.connected) {
|
||||||
} else {
|
setPollInterval(15 * 60 * 1000); // 15 minutes
|
||||||
setpollInterval(90000);
|
} else {
|
||||||
}
|
setPollInterval(60 * 1000); // 60 seconds
|
||||||
}, [fcmToken]);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSocketStatus();
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.on("connect", handleSocketStatus);
|
||||||
|
socket.on("disconnect", handleSocketStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (socket) {
|
||||||
|
socket.off("connect", handleSocketStatus);
|
||||||
|
socket.off("disconnect", handleSocketStatus);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
// Fetch conversations when chat becomes visible
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatVisible)
|
if (chatVisible)
|
||||||
getConversations({
|
getConversations({
|
||||||
variables: {
|
variables: {
|
||||||
offset: 0
|
offset: 0
|
||||||
}
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(`Error fetching conversations: ${(err, err.message || "")}`);
|
||||||
});
|
});
|
||||||
}, [chatVisible, getConversations]);
|
}, [chatVisible, getConversations]);
|
||||||
|
|
||||||
const loadMoreConversations = useCallback(() => {
|
// Get unread count from the cache
|
||||||
if (data)
|
const unreadCount = (() => {
|
||||||
fetchMore({
|
if (chatVisible) {
|
||||||
variables: {
|
try {
|
||||||
offset: data.conversations.length
|
const cachedData = client.readQuery({
|
||||||
}
|
query: CONVERSATION_LIST_QUERY,
|
||||||
});
|
variables: { offset: 0 }
|
||||||
}, [data, fetchMore]);
|
});
|
||||||
|
|
||||||
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
if (!cachedData?.conversations) return 0;
|
||||||
|
|
||||||
|
// Aggregate unread message count
|
||||||
|
return cachedData.conversations.reduce((total, conversation) => {
|
||||||
|
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
|
||||||
|
return total + unread;
|
||||||
|
}, 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Unread count not found in cache:", error);
|
||||||
|
return 0; // Fallback if not in cache
|
||||||
|
}
|
||||||
|
} else if (unreadData?.messages_aggregate?.aggregate?.count) {
|
||||||
|
// Use the unread count from the query result
|
||||||
|
return unreadData.messages_aggregate.aggregate.count;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge count={unreadCount}>
|
<Badge count={unreadCount}>
|
||||||
@@ -81,7 +122,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
<InfoCircleOutlined />
|
<InfoCircleOutlined />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<SyncOutlined style={{ cursor: "pointer" }} onClick={() => refetch()} />
|
<SyncOutlined style={{ cursor: "pointer" }} onClick={() => refetch()} />
|
||||||
{pollInterval > 0 && <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>}
|
{!socket?.connected && <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>}
|
||||||
</Space>
|
</Space>
|
||||||
<ShrinkOutlined
|
<ShrinkOutlined
|
||||||
onClick={() => toggleChatVisible()}
|
onClick={() => toggleChatVisible()}
|
||||||
@@ -93,10 +134,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
) : (
|
) : (
|
||||||
<ChatConversationListComponent
|
<ChatConversationListComponent conversationList={data ? data.conversations : []} />
|
||||||
conversationList={data ? data.conversations : []}
|
|
||||||
loadMoreConversations={loadMoreConversations}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={16}>{selectedConversation ? <ChatConversationContainer /> : null}</Col>
|
<Col span={16}>{selectedConversation ? <ChatConversationContainer /> : null}</Col>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||||
const inputArea = useRef(null);
|
const inputArea = useRef(null);
|
||||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputArea.current.focus();
|
inputArea.current.focus();
|
||||||
}, [isSending, setMessage]);
|
}, [isSending, setMessage]);
|
||||||
@@ -37,14 +38,15 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
logImEXEvent("messaging_send_message");
|
logImEXEvent("messaging_send_message");
|
||||||
|
|
||||||
if (selectedImages.length < 11) {
|
if (selectedImages.length < 11) {
|
||||||
sendMessage({
|
const newMessage = {
|
||||||
to: conversation.phone_num,
|
to: conversation.phone_num,
|
||||||
body: message || "",
|
body: message || "",
|
||||||
messagingServiceSid: bodyshop.messagingservicesid,
|
messagingServiceSid: bodyshop.messagingservicesid,
|
||||||
conversationid: conversation.id,
|
conversationid: conversation.id,
|
||||||
selectedMedia: selectedImages,
|
selectedMedia: selectedImages,
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid
|
||||||
});
|
};
|
||||||
|
sendMessage(newMessage);
|
||||||
setSelectedMedia(
|
setSelectedMedia(
|
||||||
selectedMedia.map((i) => {
|
selectedMedia.map((i) => {
|
||||||
return { ...i, isSelected: false };
|
return { ...i, isSelected: false };
|
||||||
@@ -79,7 +81,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<SendOutlined
|
<SendOutlined
|
||||||
className="imex-flex-row__margin"
|
className="chat-send-message-button"
|
||||||
// disabled={message === "" || !message}
|
// disabled={message === "" || !message}
|
||||||
onClick={handleEnter}
|
onClick={handleEnter}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,22 +2,33 @@ import { PlusOutlined } from "@ant-design/icons";
|
|||||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||||
import ChatTagRo from "./chat-tag-ro.component";
|
import ChatTagRo from "./chat-tag-ro.component";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatTagRoContainer({ conversation }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatTagRoContainer({ conversation, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
||||||
|
|
||||||
const executeSearch = (v) => {
|
const executeSearch = (v) => {
|
||||||
logImEXEvent("messaging_search_job_tag", { searchTerm: v });
|
logImEXEvent("messaging_search_job_tag", { searchTerm: v });
|
||||||
loadRo(v);
|
loadRo(v).catch((e) => console.error("Error in ChatTagRoContainer executeSearch:", e));
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||||
@@ -30,9 +41,40 @@ export default function ChatTagRoContainer({ conversation }) {
|
|||||||
variables: { conversationId: conversation.id }
|
variables: { conversationId: conversation.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleInsertTag = (value, option) => {
|
const handleInsertTag = async (value, option) => {
|
||||||
logImEXEvent("messaging_add_job_tag");
|
logImEXEvent("messaging_add_job_tag");
|
||||||
insertTag({ variables: { jobId: option.key } });
|
|
||||||
|
await insertTag({
|
||||||
|
variables: { jobId: option.key }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
// Find the job details from the search data
|
||||||
|
const selectedJob = data?.search_jobs.find((job) => job.id === option.key);
|
||||||
|
if (!selectedJob) return;
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
conversationId: conversation.id,
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
type: "tag-added",
|
||||||
|
selectedJob,
|
||||||
|
job_conversations: [
|
||||||
|
{
|
||||||
|
__typename: "job_conversations",
|
||||||
|
jobid: selectedJob.id,
|
||||||
|
conversationid: conversation.id,
|
||||||
|
job: {
|
||||||
|
__typename: "jobs",
|
||||||
|
id: selectedJob.id,
|
||||||
|
ro_number: selectedJob.ro_number,
|
||||||
|
ownr_co_nm: selectedJob.ownr_co_nm,
|
||||||
|
ownr_fn: selectedJob.ownr_fn,
|
||||||
|
ownr_ln: selectedJob.ownr_ln
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,9 +92,10 @@ export default function ChatTagRoContainer({ conversation }) {
|
|||||||
handleSearch={handleSearch}
|
handleSearch={handleSearch}
|
||||||
handleInsertTag={handleInsertTag}
|
handleInsertTag={handleInsertTag}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Tag onClick={() => setOpen(true)}>
|
<Tag style={{ cursor: "pointer" }} onClick={() => setOpen(true)}>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
{t("messaging.actions.link")}
|
{t("messaging.actions.link")}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -60,3 +103,5 @@ export default function ChatTagRoContainer({ conversation }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatTagRoContainer);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button, Divider, Dropdown, Form, Input, notification, Popover, Select,
|
|||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
@@ -24,6 +24,7 @@ import ScheduleEventNote from "./schedule-event.note.component";
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -49,6 +50,8 @@ export function ScheduleEventComponent({
|
|||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||||
const [title, setTitle] = useState(event.title);
|
const [title, setTitle] = useState(event.title);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const blockContent = (
|
const blockContent = (
|
||||||
<Space direction="vertical" wrap>
|
<Space direction="vertical" wrap>
|
||||||
<Input
|
<Input
|
||||||
@@ -190,7 +193,8 @@ export function ScheduleEventComponent({
|
|||||||
if (p && p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
openChatByPhone({
|
openChatByPhone({
|
||||||
phone_num: p.formatInternational(),
|
phone_num: p.formatInternational(),
|
||||||
jobid: event.job.id
|
jobid: event.job.id,
|
||||||
|
socket
|
||||||
});
|
});
|
||||||
setMessage(
|
setMessage(
|
||||||
t("appointments.labels.reminder", {
|
t("appointments.labels.reminder", {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Form, notification, Popover, Tooltip } from "antd";
|
import { Button, Checkbox, Form, notification, Popover, Tooltip } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|||||||
@@ -216,7 +216,9 @@ export function JobLinesComponent({
|
|||||||
{
|
{
|
||||||
title: t("joblines.fields.part_qty"),
|
title: t("joblines.fields.part_qty"),
|
||||||
dataIndex: "part_qty",
|
dataIndex: "part_qty",
|
||||||
key: "part_qty"
|
key: "part_qty",
|
||||||
|
sorter: (a, b) => a.part_qty - b.part_qty,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "part_qty" && state.sortedInfo.order
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// title: t('joblines.fields.tax_part'),
|
// title: t('joblines.fields.tax_part'),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
@@ -26,21 +25,16 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
|
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||||
setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" })),
|
setCardPaymentContext: (context) =>
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
dispatch(
|
||||||
setMessage: (text) => dispatch(setMessage(text))
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "cardPayment"
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobPayments({
|
export function JobPayments({ job, jobRO, bodyshop, setPaymentContext, setCardPaymentContext, refetch }) {
|
||||||
job,
|
|
||||||
jobRO,
|
|
||||||
bodyshop,
|
|
||||||
setMessage,
|
|
||||||
openChatByPhone,
|
|
||||||
setPaymentContext,
|
|
||||||
setCardPaymentContext,
|
|
||||||
refetch
|
|
||||||
}) {
|
|
||||||
const {
|
const {
|
||||||
treatments: { ImEXPay }
|
treatments: { ImEXPay }
|
||||||
} = useSplitTreatments({
|
} = useSplitTreatments({
|
||||||
@@ -133,7 +127,7 @@ export function JobPayments({
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
//Same as in RO guard. If changed, update in both.
|
//Same as in RO guard. If changed, update in both.
|
||||||
const total = useMemo(() => {
|
const total = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
job.payments &&
|
job.payments &&
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import JobTotalsCashDiscount from "./jobs-totals.cash-discount-display.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -22,6 +23,14 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
|||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
...(job.job_totals?.totals?.ttl_adjustment
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: `Subtotal Adj.`,
|
||||||
|
total: job.job_totals?.totals?.ttl_adjustment
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
key: t("jobs.labels.subtotal"),
|
key: t("jobs.labels.subtotal"),
|
||||||
total: job.job_totals.totals.subtotal,
|
total: job.job_totals.totals.subtotal,
|
||||||
@@ -101,7 +110,7 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
|||||||
total: job.job_totals.totals.us_sales_tax_breakdown.ty4Tax
|
total: job.job_totals.totals.us_sales_tax_breakdown.ty4Tax
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: `${bodyshop.md_responsibility_centers.taxes.tax_ty5?.tax_type5 || "TT"} - ${[
|
key: `${bodyshop.md_responsibility_centers.taxes.tax_ty5?.tax_type5 || "Adj."} - ${[
|
||||||
job.cieca_pft.ty5_rate1,
|
job.cieca_pft.ty5_rate1,
|
||||||
job.cieca_pft.ty5_rate2,
|
job.cieca_pft.ty5_rate2,
|
||||||
job.cieca_pft.ty5_rate3,
|
job.cieca_pft.ty5_rate3,
|
||||||
@@ -112,6 +121,14 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
|||||||
.join(", ")}%`,
|
.join(", ")}%`,
|
||||||
total: job.job_totals.totals.us_sales_tax_breakdown.ty5Tax
|
total: job.job_totals.totals.us_sales_tax_breakdown.ty5Tax
|
||||||
},
|
},
|
||||||
|
...(job.job_totals?.totals?.ttl_tax_adjustment
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: `Tax Adj.`,
|
||||||
|
total: job.job_totals?.totals?.ttl_tax_adjustment
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
key: t("jobs.labels.total_sales_tax"),
|
key: t("jobs.labels.total_sales_tax"),
|
||||||
bold: true,
|
bold: true,
|
||||||
@@ -120,6 +137,7 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
|||||||
.add(Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty3Tax))
|
.add(Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty3Tax))
|
||||||
.add(Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty4Tax))
|
.add(Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty4Tax))
|
||||||
.add(Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty5Tax))
|
.add(Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty5Tax))
|
||||||
|
.add(Dinero(job.job_totals.totals.ttl_tax_adjustment))
|
||||||
.toJSON()
|
.toJSON()
|
||||||
}
|
}
|
||||||
].filter((item) => item.total.amount !== 0)
|
].filter((item) => item.total.amount !== 0)
|
||||||
@@ -131,11 +149,27 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
|||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
|
|
||||||
{
|
...(bodyshop.intellipay_config?.enable_cash_discount
|
||||||
key: t("jobs.labels.total_repairs"),
|
? [
|
||||||
total: job.job_totals.totals.total_repairs,
|
{
|
||||||
bold: true
|
key: t("jobs.labels.total_repairs_cash_discount"),
|
||||||
},
|
total: job.job_totals.totals.total_repairs,
|
||||||
|
bold: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.total_repairs"),
|
||||||
|
render: <JobTotalsCashDiscount amountDinero={job.job_totals.totals.total_repairs} />,
|
||||||
|
bold: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
key: t("jobs.labels.total_repairs"),
|
||||||
|
total: job.job_totals.totals.total_repairs,
|
||||||
|
bold: true
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
|
||||||
{
|
{
|
||||||
key: t("jobs.fields.ded_amt"),
|
key: t("jobs.fields.ded_amt"),
|
||||||
total: job.job_totals.totals.custPayable.deductible
|
total: job.job_totals.totals.custPayable.deductible
|
||||||
@@ -167,13 +201,7 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: t("jobs.labels.total_cust_payable"),
|
key: t("jobs.labels.total_cust_payable"),
|
||||||
total: Dinero(job.job_totals.totals.custPayable.total)
|
render: <JobTotalsCashDiscount amountDinero={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
|
bold: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -209,7 +237,7 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
|||||||
dataIndex: "total",
|
dataIndex: "total",
|
||||||
key: "total",
|
key: "total",
|
||||||
align: "right",
|
align: "right",
|
||||||
render: (text, record) => Dinero(record.total).toFormat(),
|
render: (text, record) => (record.render ? record.render : Dinero(record.total).toFormat()),
|
||||||
width: "20%",
|
width: "20%",
|
||||||
onCell: (record, rowIndex) => {
|
onCell: (record, rowIndex) => {
|
||||||
return { style: { fontWeight: record.bold && "bold" } };
|
return { style: { fontWeight: record.bold && "bold" } };
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { notification, Spin } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobTotalsCashDiscount);
|
||||||
|
|
||||||
|
export function JobTotalsCashDiscount({ bodyshop, amountDinero }) {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [fee, setFee] = useState(0);
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
if (amountDinero && bodyshop) {
|
||||||
|
setLoading(true);
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await axios.post("/intellipay/checkfee", {
|
||||||
|
bodyshop: { id: bodyshop.id, imexshopid: bodyshop.imexshopid, state: bodyshop.state },
|
||||||
|
amount: Dinero(amountDinero).toFormat("0.00")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.data?.error) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
response.data?.error ||
|
||||||
|
"Error encountered when contacting IntelliPay service to determine cash discounted price."
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFee(response.data?.fee || 0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
error.response?.data?.error ||
|
||||||
|
"Error encountered when contacting IntelliPay service to determine cash discounted price."
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [amountDinero, bodyshop]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData, bodyshop, amountDinero]);
|
||||||
|
|
||||||
|
if (loading) return <Spin size="small" />;
|
||||||
|
return Dinero(amountDinero)
|
||||||
|
.add(Dinero({ amount: Math.round(fee * 100) }))
|
||||||
|
.toFormat();
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { gql, useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client";
|
import { gql, useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Col, Row, notification } from "antd";
|
import { Col, Row, notification } from "antd"; //import { Button, Col, Row, notification } from "antd";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
@@ -407,26 +407,25 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
updateSchComp={updateSchComp}
|
updateSchComp={updateSchComp}
|
||||||
setSchComp={setSchComp}
|
setSchComp={setSchComp}
|
||||||
/>
|
/>
|
||||||
{
|
{/* {
|
||||||
// currentUser.email.includes("@rome.") ||
|
currentUser.email.includes("@rome.") || currentUser.email.includes("@imex.") ? (
|
||||||
// currentUser.email.includes("@imex.") ? (
|
<Button
|
||||||
// <Button
|
onClick={async () => {
|
||||||
// onClick={async () => {
|
for (const record of data.available_jobs) {
|
||||||
// for (const record of data.available_jobs) {
|
//Query the data
|
||||||
// //Query the data
|
console.log("Start Job", record.id);
|
||||||
// console.log("Start Job", record.id);
|
const { data } = await loadEstData({
|
||||||
// const {data} = await loadEstData({
|
variables: { id: record.id }
|
||||||
// variables: {id: record.id},
|
});
|
||||||
// });
|
console.log("Query has been awaited and is complete");
|
||||||
// console.log("Query has been awaited and is complete");
|
await onOwnerFindModalOk(data);
|
||||||
// await onOwnerFindModalOk(data);
|
}
|
||||||
// }
|
}}
|
||||||
// }}
|
>
|
||||||
// >
|
Add all jobs as new.
|
||||||
// Add all jobs as new.
|
</Button>
|
||||||
// </Button>
|
) : null
|
||||||
// ) : null
|
} */}
|
||||||
}
|
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<JobsAvailableTableComponent
|
<JobsAvailableTableComponent
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
|||||||
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
|
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import { useMemo, useState } from "react";
|
import React, { useContext, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
@@ -30,6 +30,7 @@ import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
|||||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -126,6 +127,7 @@ export function JobsDetailHeaderActions({
|
|||||||
const [updateJob] = useMutation(UPDATE_JOB);
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
const [voidJob] = useMutation(VOID_JOB);
|
const [voidJob] = useMutation(VOID_JOB);
|
||||||
const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
|
const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { ImEXPay }
|
treatments: { ImEXPay }
|
||||||
@@ -300,7 +302,8 @@ export function JobsDetailHeaderActions({
|
|||||||
if (p && p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
openChatByPhone({
|
openChatByPhone({
|
||||||
phone_num: p.formatInternational(),
|
phone_num: p.formatInternational(),
|
||||||
jobid: job.id
|
jobid: job.id,
|
||||||
|
socket
|
||||||
});
|
});
|
||||||
setMessage(
|
setMessage(
|
||||||
`${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
|
`${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
|
||||||
@@ -343,7 +346,8 @@ export function JobsDetailHeaderActions({
|
|||||||
if (p && p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
openChatByPhone({
|
openChatByPhone({
|
||||||
phone_num: p.formatInternational(),
|
phone_num: p.formatInternational(),
|
||||||
jobid: job.id
|
jobid: job.id,
|
||||||
|
socket
|
||||||
});
|
});
|
||||||
setMessage(`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`);
|
setMessage(`${window.location.protocol}//${window.location.host}/csi/${job.csiinvites[0].id}`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { Button, Form, message, Popover, Space } from "antd";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import { parsePhoneNumber } from "libphonenumber-js";
|
import { parsePhoneNumber } from "libphonenumber-js";
|
||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -28,6 +29,7 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [paymentLink, setPaymentLink] = useState(null);
|
const [paymentLink, setPaymentLink] = useState(null);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const handleFinish = async ({ amount }) => {
|
const handleFinish = async ({ amount }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -50,7 +52,8 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope
|
|||||||
if (p) {
|
if (p) {
|
||||||
openChatByPhone({
|
openChatByPhone({
|
||||||
phone_num: p.formatInternational(),
|
phone_num: p.formatInternational(),
|
||||||
jobid: job.id
|
jobid: job.id,
|
||||||
|
socket
|
||||||
});
|
});
|
||||||
setMessage(
|
setMessage(
|
||||||
t("payments.labels.smspaymentreminder", {
|
t("payments.labels.smspaymentreminder", {
|
||||||
@@ -106,7 +109,8 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope
|
|||||||
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
const p = parsePhoneNumber(job.ownr_ph1, "CA");
|
||||||
openChatByPhone({
|
openChatByPhone({
|
||||||
phone_num: p.formatInternational(),
|
phone_num: p.formatInternational(),
|
||||||
jobid: job.id
|
jobid: job.id,
|
||||||
|
socket
|
||||||
});
|
});
|
||||||
setMessage(
|
setMessage(
|
||||||
t("payments.labels.smspaymentreminder", {
|
t("payments.labels.smspaymentreminder", {
|
||||||
|
|||||||
@@ -274,6 +274,22 @@ export function ReportCenterModalComponent({ reportCenterModal, bodyshop }) {
|
|||||||
{
|
{
|
||||||
required: true
|
required: true
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: (_, value) => {
|
||||||
|
if (
|
||||||
|
(!import.meta.env.VITE_APP_IS_TEST && import.meta.env.PROD) &&
|
||||||
|
value &&
|
||||||
|
value[0] &&
|
||||||
|
value[1]
|
||||||
|
) {
|
||||||
|
const diffInDays = (value[1] - value[0]) / (1000 * 3600 * 24);
|
||||||
|
if (diffInDays > 92) {
|
||||||
|
return Promise.reject(t("general.validation.dateRangeExceeded"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4333,6 +4333,70 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
|
{InstanceRenderManager({
|
||||||
|
promanager: "USE_ROME",
|
||||||
|
rome: (
|
||||||
|
<LayoutFormRow header={<div>Adjustments</div>} id="refund">
|
||||||
|
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber ? (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.responsibilitycenters.ttl_adjustment")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["md_responsibility_centers", "ttl_adjustment", "dms_acctnumber"]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.responsibilitycenters.ttl_tax_adjustment")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["md_responsibility_centers", "ttl_tax_adjustment", "dms_acctnumber"]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.responsibilitycenters.ttl_adjustment")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["md_responsibility_centers", "ttl_adjustment", "accountitem"]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.labels.responsibilitycenters.ttl_tax_adjustment")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
name={["md_responsibility_centers", "ttl_tax_adjustment", "accountitem"]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</LayoutFormRow>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{Qb_Multi_Ar.treatment === "on" && (
|
{Qb_Multi_Ar.treatment === "on" && (
|
||||||
<LayoutFormRow header={<div>Multiple Payers Item</div>} id="accountitem">
|
<LayoutFormRow header={<div>Multiple Payers Item</div>} id="accountitem">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -37,16 +37,6 @@ export function ShopInfoIntellipay({ bodyshop, form }) {
|
|||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
label={t("bodyshop.fields.intellipay_config.cash_discount_percentage")}
|
|
||||||
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="%" />
|
|
||||||
</Form.Item>
|
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { GlobalOutlined } from "@ant-design/icons";
|
import { GlobalOutlined, WarningOutlined } from "@ant-design/icons";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { selectWssStatus } from "../../redux/application/application.selectors";
|
import { selectWssStatus } from "../../redux/application/application.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
|
||||||
wssStatus: selectWssStatus
|
wssStatus: selectWssStatus
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
});
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay);
|
|
||||||
|
|
||||||
export function WssStatusDisplay({ wssStatus }) {
|
export function WssStatusDisplay({ wssStatus }) {
|
||||||
console.log("🚀 ~ WssStatusDisplay ~ wssStatus:", wssStatus);
|
console.log("🚀 ~ WssStatusDisplay ~ wssStatus:", wssStatus);
|
||||||
return <GlobalOutlined style={{ color: wssStatus === "connected" ? "green" : "red", marginRight: ".5rem" }} />;
|
|
||||||
|
let icon;
|
||||||
|
let color;
|
||||||
|
|
||||||
|
if (wssStatus === "connected") {
|
||||||
|
icon = <GlobalOutlined />;
|
||||||
|
color = "green";
|
||||||
|
} else if (wssStatus === "error") {
|
||||||
|
icon = <WarningOutlined />;
|
||||||
|
color = "red";
|
||||||
|
} else {
|
||||||
|
icon = <GlobalOutlined />;
|
||||||
|
color = "gray"; // Default for other statuses like "disconnected"
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span style={{ color, marginRight: ".5rem" }}>{icon}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(WssStatusDisplay);
|
||||||
|
|||||||
@@ -9,68 +9,96 @@ const useSocket = (bodyshop) => {
|
|||||||
const [clientId, setClientId] = useState(null);
|
const [clientId, setClientId] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const initializeSocket = async (token) => {
|
||||||
|
if (!bodyshop || !bodyshop.id) return;
|
||||||
|
|
||||||
|
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
|
||||||
|
|
||||||
|
const socketInstance = SocketIO(endpoint, {
|
||||||
|
path: "/wss",
|
||||||
|
withCredentials: true,
|
||||||
|
auth: { token },
|
||||||
|
reconnectionAttempts: Infinity,
|
||||||
|
reconnectionDelay: 2000,
|
||||||
|
reconnectionDelayMax: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current = socketInstance;
|
||||||
|
|
||||||
|
// Handle socket events
|
||||||
|
const handleBodyshopMessage = (message) => {
|
||||||
|
if (!message || !message.type) return;
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case "alert-update":
|
||||||
|
store.dispatch(addAlerts(message.payload));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!import.meta.env.DEV) return;
|
||||||
|
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConnect = () => {
|
||||||
|
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||||
|
setClientId(socketInstance.id);
|
||||||
|
store.dispatch(setWssStatus("connected"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReconnect = () => {
|
||||||
|
store.dispatch(setWssStatus("connected"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConnectionError = (err) => {
|
||||||
|
console.error("Socket connection error:", err);
|
||||||
|
|
||||||
|
// Handle token expiration
|
||||||
|
if (err.message.includes("auth/id-token-expired")) {
|
||||||
|
console.warn("Token expired, refreshing...");
|
||||||
|
auth.currentUser?.getIdToken(true).then((newToken) => {
|
||||||
|
socketInstance.auth = { token: newToken }; // Update socket auth
|
||||||
|
socketInstance.connect(); // Retry connection
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
store.dispatch(setWssStatus("error"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisconnect = (reason) => {
|
||||||
|
console.warn("Socket disconnected:", reason);
|
||||||
|
store.dispatch(setWssStatus("disconnected"));
|
||||||
|
|
||||||
|
// Manually trigger reconnection if necessary
|
||||||
|
if (!socketInstance.connected && reason !== "io server disconnect") {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (socketInstance.disconnected) {
|
||||||
|
console.log("Manually triggering reconnection...");
|
||||||
|
socketInstance.connect();
|
||||||
|
}
|
||||||
|
}, 2000); // Retry after 2 seconds
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register event handlers
|
||||||
|
socketInstance.on("connect", handleConnect);
|
||||||
|
socketInstance.on("reconnect", handleReconnect);
|
||||||
|
socketInstance.on("connect_error", handleConnectionError);
|
||||||
|
socketInstance.on("disconnect", handleDisconnect);
|
||||||
|
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
||||||
|
};
|
||||||
|
|
||||||
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
const newToken = await user.getIdToken();
|
const token = await user.getIdToken();
|
||||||
|
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
// Send new token to server
|
// Update token if socket exists
|
||||||
socketRef.current.emit("update-token", newToken);
|
socketRef.current.emit("update-token", token);
|
||||||
} else if (bodyshop && bodyshop.id) {
|
} else {
|
||||||
// Initialize the socket
|
// Initialize socket if not already connected
|
||||||
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
|
initializeSocket(token);
|
||||||
|
|
||||||
const socketInstance = SocketIO(endpoint, {
|
|
||||||
path: "/wss",
|
|
||||||
withCredentials: true,
|
|
||||||
auth: { token: newToken },
|
|
||||||
reconnectionAttempts: Infinity,
|
|
||||||
reconnectionDelay: 2000,
|
|
||||||
reconnectionDelayMax: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConnect = () => {
|
|
||||||
console.log("Socket connected:", socketInstance.id);
|
|
||||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
|
||||||
setClientId(socketInstance.id);
|
|
||||||
store.dispatch(setWssStatus("connected"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReconnect = (attempt) => {
|
|
||||||
console.log(`Socket reconnected after ${attempt} attempts`);
|
|
||||||
store.dispatch(setWssStatus("connected"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConnectionError = (err) => {
|
|
||||||
console.error("Socket connection error:", err);
|
|
||||||
store.dispatch(setWssStatus("error"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
|
||||||
console.log("Socket disconnected");
|
|
||||||
store.dispatch(setWssStatus("disconnected"));
|
|
||||||
};
|
|
||||||
|
|
||||||
socketInstance.on("connect", handleConnect);
|
|
||||||
socketInstance.on("reconnect", handleReconnect);
|
|
||||||
socketInstance.on("connect_error", handleConnectionError);
|
|
||||||
socketInstance.on("disconnect", handleDisconnect);
|
|
||||||
socketInstance.on("bodyshop-message", handleBodyshopMessage);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// User is not authenticated
|
// User is not authenticated
|
||||||
@@ -81,7 +109,7 @@ const useSocket = (bodyshop) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up the listener on unmount
|
// Clean up on unmount
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { getAuth, updatePassword, updateProfile } from "firebase/auth";
|
|||||||
import { getFirestore } from "firebase/firestore";
|
import { getFirestore } from "firebase/firestore";
|
||||||
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
||||||
import { store } from "../redux/store";
|
import { store } from "../redux/store";
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||||
initializeApp(config);
|
initializeApp(config);
|
||||||
@@ -66,13 +65,10 @@ export const requestForToken = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onMessageListener = () =>
|
onMessage(messaging, (payload) => {
|
||||||
new Promise((resolve) => {
|
console.log("FCM Message received. ", payload);
|
||||||
onMessage(messaging, (payload) => {
|
// ...
|
||||||
console.log("Inbound FCM Message", payload);
|
});
|
||||||
resolve(payload);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||||
const state = stateProp || store.getState();
|
const state = stateProp || store.getState();
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export const GET_CONVERSATION_DETAILS = gql`
|
|||||||
id
|
id
|
||||||
phone_num
|
phone_num
|
||||||
archived
|
archived
|
||||||
|
updated_at
|
||||||
|
unreadcnt
|
||||||
label
|
label
|
||||||
job_conversations {
|
job_conversations {
|
||||||
jobid
|
jobid
|
||||||
@@ -71,6 +73,17 @@ export const GET_CONVERSATION_DETAILS = gql`
|
|||||||
ro_number
|
ro_number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
messages(order_by: { created_at: asc_nulls_first }) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
text
|
||||||
|
isoutbound
|
||||||
|
image
|
||||||
|
image_path
|
||||||
|
userid
|
||||||
|
created_at
|
||||||
|
read
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -79,9 +92,26 @@ export const CONVERSATION_ID_BY_PHONE = gql`
|
|||||||
query CONVERSATION_ID_BY_PHONE($phone: String!) {
|
query CONVERSATION_ID_BY_PHONE($phone: String!) {
|
||||||
conversations(where: { phone_num: { _eq: $phone } }) {
|
conversations(where: { phone_num: { _eq: $phone } }) {
|
||||||
id
|
id
|
||||||
|
phone_num
|
||||||
|
archived
|
||||||
|
label
|
||||||
|
unreadcnt
|
||||||
|
created_at
|
||||||
job_conversations {
|
job_conversations {
|
||||||
jobid
|
jobid
|
||||||
id
|
conversationid
|
||||||
|
job {
|
||||||
|
id
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
ro_number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false } }) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +122,26 @@ export const CREATE_CONVERSATION = gql`
|
|||||||
insert_conversations(objects: $conversation) {
|
insert_conversations(objects: $conversation) {
|
||||||
returning {
|
returning {
|
||||||
id
|
id
|
||||||
|
phone_num
|
||||||
|
archived
|
||||||
|
label
|
||||||
|
unreadcnt
|
||||||
|
job_conversations {
|
||||||
|
jobid
|
||||||
|
conversationid
|
||||||
|
job {
|
||||||
|
id
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
ro_number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false } }) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { gql } from "@apollo/client";
|
|||||||
|
|
||||||
export const GET_ALL_JOBLINES_BY_PK = gql`
|
export const GET_ALL_JOBLINES_BY_PK = gql`
|
||||||
query GET_ALL_JOBLINES_BY_PK($id: uuid!) {
|
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
|
id
|
||||||
line_no
|
line_no
|
||||||
unq_seq
|
unq_seq
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchAlerts();
|
fetchAlerts();
|
||||||
}, []);
|
}, [setAlerts]);
|
||||||
|
|
||||||
// Use useEffect to watch for new alerts
|
// Use useEffect to watch for new alerts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -645,7 +645,7 @@ export function Manage({ conflict, bodyshop, alerts, setAlerts }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
|
<ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />
|
||||||
<Layout style={{ minHeight: "100vh" }} className="layout-container">
|
<Layout style={{ minHeight: "100vh" }} className="layout-container">
|
||||||
<UpdateAlert />
|
<UpdateAlert />
|
||||||
<HeaderContainer />
|
<HeaderContainer />
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import axios from "axios";
|
|||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { CONVERSATION_ID_BY_PHONE, CREATE_CONVERSATION } from "../../graphql/conversations.queries";
|
import {
|
||||||
|
CONVERSATION_ID_BY_PHONE,
|
||||||
|
CREATE_CONVERSATION,
|
||||||
|
TOGGLE_CONVERSATION_ARCHIVE
|
||||||
|
} from "../../graphql/conversations.queries";
|
||||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import client from "../../utils/GraphQLClient";
|
import client from "../../utils/GraphQLClient";
|
||||||
import { selectBodyshop } from "../user/user.selectors";
|
import { selectBodyshop } from "../user/user.selectors";
|
||||||
@@ -27,23 +31,73 @@ export function* onOpenChatByPhone() {
|
|||||||
|
|
||||||
export function* openChatByPhone({ payload }) {
|
export function* openChatByPhone({ payload }) {
|
||||||
logImEXEvent("messaging_open_by_phone");
|
logImEXEvent("messaging_open_by_phone");
|
||||||
const { phone_num, jobid } = payload;
|
const { socket, phone_num, jobid } = payload;
|
||||||
|
if (!socket || !phone_num) return;
|
||||||
|
|
||||||
const p = parsePhoneNumber(phone_num, "CA");
|
const p = parsePhoneNumber(phone_num, "CA");
|
||||||
const bodyshop = yield select(selectBodyshop);
|
const bodyshop = yield select(selectBodyshop);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Fetch conversations including archived ones
|
||||||
const {
|
const {
|
||||||
data: { conversations }
|
data: { conversations }
|
||||||
} = yield client.query({
|
} = yield client.query({
|
||||||
query: CONVERSATION_ID_BY_PHONE,
|
query: CONVERSATION_ID_BY_PHONE,
|
||||||
variables: { phone: p.number },
|
variables: { phone: p.number },
|
||||||
fetchPolicy: 'no-cache'
|
fetchPolicy: "no-cache" // Ensure the query always gets the latest data
|
||||||
});
|
});
|
||||||
|
|
||||||
if (conversations.length === 0) {
|
// Sort conversations by `updated_at` or `created_at` and pick the last one for the given phone number
|
||||||
|
const sortedConversations = conversations
|
||||||
|
?.filter((c) => c.phone_num === p.number) // Filter to match the phone number
|
||||||
|
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); // Sort by `updated_at`
|
||||||
|
|
||||||
|
const existingConversation = sortedConversations?.[sortedConversations.length - 1] || null;
|
||||||
|
|
||||||
|
if (existingConversation) {
|
||||||
|
let updatedConversation = existingConversation;
|
||||||
|
|
||||||
|
if (existingConversation.archived) {
|
||||||
|
// If the conversation is archived, unarchive it
|
||||||
|
const {
|
||||||
|
data: { update_conversations_by_pk: unarchivedConversation }
|
||||||
|
} = yield client.mutate({
|
||||||
|
mutation: TOGGLE_CONVERSATION_ARCHIVE,
|
||||||
|
variables: {
|
||||||
|
id: existingConversation.id,
|
||||||
|
archived: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updatedConversation = unarchivedConversation;
|
||||||
|
|
||||||
|
// Emit an event indicating the conversation was unarchived
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
type: "conversation-unarchived",
|
||||||
|
conversationId: unarchivedConversation.id,
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
archived: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the unarchived or already active conversation as selected
|
||||||
|
yield put(setSelectedConversation(updatedConversation.id));
|
||||||
|
|
||||||
|
// Add job tag if needed
|
||||||
|
if (jobid && !updatedConversation.job_conversations.find((jc) => jc.jobid === jobid)) {
|
||||||
|
yield client.mutate({
|
||||||
|
mutation: INSERT_CONVERSATION_TAG,
|
||||||
|
variables: {
|
||||||
|
conversationId: updatedConversation.id,
|
||||||
|
jobId: jobid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No conversation exists, create a new one
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
insert_conversations: { returning: newConversationsId }
|
insert_conversations: { returning: newConversations }
|
||||||
}
|
}
|
||||||
} = yield client.mutate({
|
} = yield client.mutate({
|
||||||
mutation: CREATE_CONVERSATION,
|
mutation: CREATE_CONVERSATION,
|
||||||
@@ -57,26 +111,21 @@ export function* openChatByPhone({ payload }) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
yield put(setSelectedConversation(newConversationsId[0].id));
|
|
||||||
} else if (conversations.length === 1) {
|
|
||||||
//got the ID. Open it.
|
|
||||||
yield put(setSelectedConversation(conversations[0].id));
|
|
||||||
|
|
||||||
//Check to see if this job ID is already a child of it. If not add the tag.
|
const createdConversation = newConversations[0];
|
||||||
if (jobid && !conversations[0].job_conversations.find((jc) => jc.jobid === jobid))
|
|
||||||
yield client.mutate({
|
// Emit event for the new conversation with full details
|
||||||
mutation: INSERT_CONVERSATION_TAG,
|
socket.emit("conversation-modified", {
|
||||||
variables: {
|
bodyshopId: bodyshop.id,
|
||||||
conversationId: conversations[0].id,
|
type: "conversation-created",
|
||||||
jobId: jobid
|
...createdConversation
|
||||||
}
|
});
|
||||||
});
|
|
||||||
} else {
|
// Set the newly created conversation as selected
|
||||||
console.log("ERROR: Multiple conversations found. ");
|
yield put(setSelectedConversation(createdConversation.id));
|
||||||
yield put(setSelectedConversation(null));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error in sendMessage saga.", error);
|
console.error("Error in openChatByPhone saga.", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -694,8 +694,8 @@
|
|||||||
"sales_tax_codes": "Sales Tax Codes",
|
"sales_tax_codes": "Sales Tax Codes",
|
||||||
"tax_accounts": "Tax Accounts",
|
"tax_accounts": "Tax Accounts",
|
||||||
"title": "Responsibility Centers",
|
"title": "Responsibility Centers",
|
||||||
"ttl_adjustment": "",
|
"ttl_adjustment": "Subtotal Adjustment Account",
|
||||||
"ttl_tax_adjustment": ""
|
"ttl_tax_adjustment": "Tax Adjustment Account"
|
||||||
},
|
},
|
||||||
"roguard": {
|
"roguard": {
|
||||||
"title": "RO Guard"
|
"title": "RO Guard"
|
||||||
@@ -1290,6 +1290,7 @@
|
|||||||
"unsavedchangespopup": "You have unsaved changes. Are you sure you want to leave?"
|
"unsavedchangespopup": "You have unsaved changes. Are you sure you want to leave?"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
"dateRangeExceeded": "The date range has been exceeded.",
|
||||||
"invalidemail": "Please enter a valid email.",
|
"invalidemail": "Please enter a valid email.",
|
||||||
"invalidphone": "Please enter a valid phone number.",
|
"invalidphone": "Please enter a valid phone number.",
|
||||||
"required": "{{label}} is required."
|
"required": "{{label}} is required."
|
||||||
@@ -2117,6 +2118,7 @@
|
|||||||
"total_cust_payable": "Total Customer Amount Payable",
|
"total_cust_payable": "Total Customer Amount Payable",
|
||||||
"total_cust_payable_cash_discount": "$t(jobs.labels.total_cust_payable) (Cash Discounted)",
|
"total_cust_payable_cash_discount": "$t(jobs.labels.total_cust_payable) (Cash Discounted)",
|
||||||
"total_repairs": "Total Repairs",
|
"total_repairs": "Total Repairs",
|
||||||
|
"total_repairs_cash_discount": "Total Repairs (Cash Discounted)",
|
||||||
"total_sales": "Total Sales",
|
"total_sales": "Total Sales",
|
||||||
"total_sales_tax": "Total Sales Tax",
|
"total_sales_tax": "Total Sales Tax",
|
||||||
"totals": "Totals",
|
"totals": "Totals",
|
||||||
|
|||||||
@@ -1290,6 +1290,7 @@
|
|||||||
"unsavedchangespopup": ""
|
"unsavedchangespopup": ""
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
"dateRangeExceeded": "",
|
||||||
"invalidemail": "Por favor introduzca una dirección de correo electrónico válida.",
|
"invalidemail": "Por favor introduzca una dirección de correo electrónico válida.",
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
"required": "Este campo es requerido."
|
"required": "Este campo es requerido."
|
||||||
@@ -2117,6 +2118,7 @@
|
|||||||
"total_cust_payable": "",
|
"total_cust_payable": "",
|
||||||
"total_cust_payable_cash_discount": "",
|
"total_cust_payable_cash_discount": "",
|
||||||
"total_repairs": "",
|
"total_repairs": "",
|
||||||
|
"total_repairs_cash_discount": "",
|
||||||
"total_sales": "",
|
"total_sales": "",
|
||||||
"total_sales_tax": "",
|
"total_sales_tax": "",
|
||||||
"totals": "",
|
"totals": "",
|
||||||
|
|||||||
@@ -1290,6 +1290,7 @@
|
|||||||
"unsavedchangespopup": ""
|
"unsavedchangespopup": ""
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
|
"dateRangeExceeded": "",
|
||||||
"invalidemail": "S'il vous plaît entrer un email valide.",
|
"invalidemail": "S'il vous plaît entrer un email valide.",
|
||||||
"invalidphone": "",
|
"invalidphone": "",
|
||||||
"required": "Ce champ est requis."
|
"required": "Ce champ est requis."
|
||||||
@@ -2117,6 +2118,7 @@
|
|||||||
"total_cust_payable": "",
|
"total_cust_payable": "",
|
||||||
"total_cust_payable_cash_discount": "",
|
"total_cust_payable_cash_discount": "",
|
||||||
"total_repairs": "",
|
"total_repairs": "",
|
||||||
|
"total_repairs_cash_discount": "",
|
||||||
"total_sales": "",
|
"total_sales": "",
|
||||||
"total_sales_tax": "",
|
"total_sales_tax": "",
|
||||||
"totals": "",
|
"totals": "",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { setContext } from "@apollo/client/link/context";
|
|||||||
import { HttpLink } from "@apollo/client/link/http"; //"apollo-link-http";
|
import { HttpLink } from "@apollo/client/link/http"; //"apollo-link-http";
|
||||||
import { RetryLink } from "@apollo/client/link/retry";
|
import { RetryLink } from "@apollo/client/link/retry";
|
||||||
import { WebSocketLink } from "@apollo/client/link/ws";
|
import { WebSocketLink } from "@apollo/client/link/ws";
|
||||||
import { getMainDefinition, offsetLimitPagination } from "@apollo/client/utilities";
|
import { getMainDefinition } from "@apollo/client/utilities";
|
||||||
//import { split } from "apollo-link";
|
//import { split } from "apollo-link";
|
||||||
import apolloLogger from "apollo-link-logger";
|
import apolloLogger from "apollo-link-logger";
|
||||||
//import axios from "axios";
|
//import axios from "axios";
|
||||||
@@ -143,16 +143,7 @@ middlewares.push(
|
|||||||
new SentryLink().concat(roundTripLink.concat(retryLink.concat(errorLink.concat(authLink.concat(link)))))
|
new SentryLink().concat(roundTripLink.concat(retryLink.concat(errorLink.concat(authLink.concat(link)))))
|
||||||
);
|
);
|
||||||
|
|
||||||
const cache = new InMemoryCache({
|
const cache = new InMemoryCache({});
|
||||||
typePolicies: {
|
|
||||||
Query: {
|
|
||||||
fields: {
|
|
||||||
conversations: offsetLimitPagination()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = new ApolloClient({
|
const client = new ApolloClient({
|
||||||
link: ApolloLink.from(middlewares),
|
link: ApolloLink.from(middlewares),
|
||||||
cache,
|
cache,
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
export default async function FcmHandler({ client, payload }) {
|
|
||||||
console.log("FCM", payload);
|
|
||||||
switch (payload.type) {
|
|
||||||
case "messaging-inbound":
|
|
||||||
client.cache.modify({
|
|
||||||
id: client.cache.identify({
|
|
||||||
__typename: "conversations",
|
|
||||||
id: payload.conversationid
|
|
||||||
}),
|
|
||||||
fields: {
|
|
||||||
messages_aggregate(cached) {
|
|
||||||
return { aggregate: { count: cached.aggregate.count + 1 } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
client.cache.modify({
|
|
||||||
fields: {
|
|
||||||
messages_aggregate(cached) {
|
|
||||||
return { aggregate: { count: cached.aggregate.count + 1 } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "messaging-outbound":
|
|
||||||
client.cache.modify({
|
|
||||||
id: client.cache.identify({
|
|
||||||
__typename: "conversations",
|
|
||||||
id: payload.conversationid
|
|
||||||
}),
|
|
||||||
fields: {
|
|
||||||
updated_at(oldupdated0) {
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
// messages_aggregate(cached) {
|
|
||||||
// return { aggregate: { count: cached.aggregate.count + 1 } };
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "messaging-mark-conversation-read":
|
|
||||||
let previousUnreadCount = 0;
|
|
||||||
client.cache.modify({
|
|
||||||
id: client.cache.identify({
|
|
||||||
__typename: "conversations",
|
|
||||||
id: payload.conversationid
|
|
||||||
}),
|
|
||||||
fields: {
|
|
||||||
messages_aggregate(cached) {
|
|
||||||
previousUnreadCount = cached.aggregate.count;
|
|
||||||
return { aggregate: { count: 0 } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
client.cache.modify({
|
|
||||||
fields: {
|
|
||||||
messages_aggregate(cached) {
|
|
||||||
return {
|
|
||||||
aggregate: {
|
|
||||||
count: cached.aggregate.count - previousUnreadCount
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("No payload type set.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { promises as fsPromises } from "fs";
|
import { promises as fsPromises } from "fs";
|
||||||
import { createRequire } from "module";
|
|
||||||
import * as path from "path";
|
|
||||||
import * as url from "url";
|
|
||||||
import { createLogger, defineConfig } from "vite";
|
import { createLogger, defineConfig } from "vite";
|
||||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||||
import eslint from "vite-plugin-eslint";
|
import eslint from "vite-plugin-eslint";
|
||||||
@@ -18,28 +15,6 @@ process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
|||||||
const getFormattedTimestamp = () =>
|
const getFormattedTimestamp = () =>
|
||||||
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
|
||||||
|
|
||||||
/** This is a hack around react-virtualized, should be removed when switching to react-virtuoso */
|
|
||||||
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
|
|
||||||
|
|
||||||
function reactVirtualizedFix() {
|
|
||||||
return {
|
|
||||||
name: "flat:react-virtualized",
|
|
||||||
configResolved: async () => {
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const reactVirtualizedPath = require.resolve("react-virtualized");
|
|
||||||
const { pathname: reactVirtualizedFilePath } = new url.URL(reactVirtualizedPath, import.meta.url);
|
|
||||||
const file = reactVirtualizedFilePath.replace(
|
|
||||||
path.join("dist", "commonjs", "index.js"),
|
|
||||||
path.join("dist", "es", "WindowScroller", "utils", "onScroll.js")
|
|
||||||
);
|
|
||||||
const code = await fsPromises.readFile(file, "utf-8");
|
|
||||||
const modified = code.replace(WRONG_CODE, "");
|
|
||||||
await fsPromises.writeFile(file, modified);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/** End of hack */
|
|
||||||
|
|
||||||
export const logger = createLogger("info", {
|
export const logger = createLogger("info", {
|
||||||
allowClearScreen: false
|
allowClearScreen: false
|
||||||
});
|
});
|
||||||
@@ -104,7 +79,6 @@ export default defineConfig({
|
|||||||
gcm_sender_id: "103953800507"
|
gcm_sender_id: "103953800507"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
reactVirtualizedFix(),
|
|
||||||
react(),
|
react(),
|
||||||
eslint()
|
eslint()
|
||||||
],
|
],
|
||||||
|
|||||||
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
|
||||||
@@ -114,8 +114,8 @@ services:
|
|||||||
"
|
"
|
||||||
aws --endpoint-url=http://localstack:4566 ses verify-domain-identity --domain imex.online --region ca-central-1
|
aws --endpoint-url=http://localstack:4566 ses verify-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 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 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
|
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: The Main IMEX API
|
||||||
@@ -169,7 +169,7 @@ services:
|
|||||||
# - redis-insight-data:/db
|
# - redis-insight-data:/db
|
||||||
|
|
||||||
# ##Optional Container for SFTP/SSH Server for testing
|
# ##Optional Container for SFTP/SSH Server for testing
|
||||||
# ssh-sftp-server:
|
# ssh-sftp-server:
|
||||||
# image: atmoz/sftp:alpine # Using an image with SFTP support
|
# image: atmoz/sftp:alpine # Using an image with SFTP support
|
||||||
# container_name: ssh-sftp-server
|
# container_name: ssh-sftp-server
|
||||||
# hostname: ssh-sftp-server
|
# hostname: ssh-sftp-server
|
||||||
@@ -178,9 +178,10 @@ services:
|
|||||||
# ports:
|
# ports:
|
||||||
# - "2222:22" # Expose port 22 for SSH/SFTP (mapped to 2222 on the host)
|
# - "2222:22" # Expose port 22 for SSH/SFTP (mapped to 2222 on the host)
|
||||||
# volumes:
|
# volumes:
|
||||||
# - ./certs/id_rsa.pub:/home/user/.ssh/keys/id_rsa.pub:ro # Mount the SSH public key
|
# - ./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
|
# - ./upload:/home/user/upload # Mount a local directory for SFTP uploads
|
||||||
# environment:
|
# environment:
|
||||||
|
# # - SFTP_USERS=user::1000::upload
|
||||||
# - SFTP_USERS=user:password:1000::upload
|
# - SFTP_USERS=user:password:1000::upload
|
||||||
# command: >
|
# command: >
|
||||||
# /bin/sh -c "
|
# /bin/sh -c "
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
headers:
|
headers:
|
||||||
- name: x-imex-auth
|
- name: x-imex-auth
|
||||||
value_from_env: DATAPUMP_AUTH
|
value_from_env: DATAPUMP_AUTH
|
||||||
|
- name: Chatter Data Pump
|
||||||
|
webhook: '{{HASURA_API_URL}}/data/chatter'
|
||||||
|
schedule: 45 5 * * *
|
||||||
|
include_in_metadata: true
|
||||||
|
payload: {}
|
||||||
|
headers:
|
||||||
|
- name: x-imex-auth
|
||||||
|
value_from_env: DATAPUMP_AUTH
|
||||||
|
comment: ""
|
||||||
- name: Claimscorp Data Pump
|
- name: Claimscorp Data Pump
|
||||||
webhook: '{{HASURA_API_URL}}/data/cc'
|
webhook: '{{HASURA_API_URL}}/data/cc'
|
||||||
schedule: 30 6 * * *
|
schedule: 30 6 * * *
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
const Dinero = require("dinero.js");
|
const Dinero = require("dinero.js");
|
||||||
const { gql } = require("graphql-request");
|
const { gql } = require("graphql-request");
|
||||||
const queries = require("./server/graphql-client/queries");
|
|
||||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
|
||||||
const logger = require("./server/utils/logger");
|
|
||||||
const AxiosLib = require("axios").default;
|
const AxiosLib = require("axios").default;
|
||||||
const axios = AxiosLib.create();
|
const axios = AxiosLib.create();
|
||||||
|
const pLimit = require("p-limit");
|
||||||
|
const converter = require("json-2-csv");
|
||||||
|
|
||||||
// Dinero.defaultCurrency = "USD";
|
|
||||||
// Dinero.globalLocale = "en-CA";
|
|
||||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
const client = require("./server/graphql-client/graphql-client").client;
|
const client = require("./server/graphql-client/graphql-client").client;
|
||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
@@ -16,8 +14,9 @@ require("dotenv").config({
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function RunTheTest() {
|
async function RunTheTest() {
|
||||||
const bodyshopids = ["b501bb82-22b2-493a-8a0f-152938194869"];
|
const bodyshopids = ["71f8494c-89f0-43e0-8eb2-820b52d723bc"];
|
||||||
const bearerToken = `Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImJhNjI1OTZmNTJmNTJlZDQ0MDQ5Mzk2YmU3ZGYzNGQyYzY0ZjQ1M2UiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiUm9tZSBEZXZlbG9wbWVudCIsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoidXNlciIsIngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsidXNlciJdLCJ4LWhhc3VyYS11c2VyLWlkIjoidDZZbTFORGxDRE9QWnIzRjliZ3VXSDRMaFNYMiJ9LCJpb2FkbWluIjp0cnVlLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vcm9tZS1wcm9kLTEiLCJhdWQiOiJyb21lLXByb2QtMSIsImF1dGhfdGltZSI6MTcxMDk1MTg1MCwidXNlcl9pZCI6InQ2WW0xTkRsQ0RPUFpyM0Y5Ymd1V0g0TGhTWDIiLCJzdWIiOiJ0NlltMU5EbENET1BacjNGOWJndVdINExoU1gyIiwiaWF0IjoxNzExNTczODI1LCJleHAiOjE3MTE1Nzc0MjUsImVtYWlsIjoicGF0cmlja0Byb21lLmRldiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJwYXRyaWNrQHJvbWUuZGV2Il19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.0kBySA9tJznLYj8TtncHGVWJO0IcmLKP2G1UyyXwaj45kTa25bjT9RWjM-NslX_zjOvrvmQZzisFAb6M1Jf6geNjOMLIqb8bhihhzEZK4CcRfvjT6cpZxnOO2Dp_1Y5OePbvOBS_GlfdsovVWa84OLuhYC5G_3QwHT8_2Cttz4CbrC6M_vd7QsGODJYBbVKMhOdZhzpNq7AbOUh3749WRjLMMobpnZDrmQlsyg3PAqtX1FHO25WQS2rma9QahGDSY736JfbkuZJ2XbNn0axEGpK7RQLUcuRkFUlfKqYplNbR_e1Q3kEfRAZpxBPXZysrDcbDNhbkWCoTmJ3fle55OA`;
|
const bearerToken = `Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImI4Y2FjOTViNGE1YWNkZTBiOTY1NzJkZWU4YzhjOTVlZWU0OGNjY2QiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiUGF0cmljayBGaWMgKERFVikiLCJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLWRlZmF1bHQtcm9sZSI6InVzZXIiLCJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbInVzZXIiXSwieC1oYXN1cmEtdXNlci1pZCI6ImhOSjhBRHB0REhRQkRFcXNCOFFNWVRqaURuZjEifSwiaXNzIjoiaHR0cHM6Ly9zZWN1cmV0b2tlbi5nb29nbGUuY29tL2ltZXgtZGV2IiwiYXVkIjoiaW1leC1kZXYiLCJhdXRoX3RpbWUiOjE3MzAxMzIwMjksInVzZXJfaWQiOiJoTko4QURwdERIUUJERXFzQjhRTVlUamlEbmYxIiwic3ViIjoiaE5KOEFEcHRESFFCREVxc0I4UU1ZVGppRG5mMSIsImlhdCI6MTczMDg0MTc2NSwiZXhwIjoxNzMwODQ1MzY1LCJlbWFpbCI6InBhdHJpY2tAaW1leC5kZXYiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsicGF0cmlja0BpbWV4LmRldiJdfSwic2lnbl9pbl9wcm92aWRlciI6InBhc3N3b3JkIn19.npQWkyB5cB4wmkaBsQiY3JbvBM9vKPqf3e22nVHnSydGcQi0p9M2mca9FcDtdcWvQlShUM63FF-6KkzpovC92sHauNmzCSXRInaaCPEussUUNSJEe2gEV03tYX447LkkSmFQbJ5V6qLTIDelm25fF0MoEDVnLTgythK_9927f8cxKZH1kEow0ymDeMaWey1sRyu7n15OJMcu692mfuQnBAArGTHGJ4YmReI7tMmdrV438MLxuVpH5CLb6uzlUdZoJ__7yh0kz0lkZEeHQAL8yq-0fISbPeZ5uXuMzYGrHuuKsIPRoeShVSVnF7ov8yTT3_YrCkhYbxl0eSTfBB5OdQ`;
|
||||||
|
|
||||||
const { jobs } = await client.request(
|
const { jobs } = await client.request(
|
||||||
gql`
|
gql`
|
||||||
query GET_JOBS($bodyshopids: [uuid!]!) {
|
query GET_JOBS($bodyshopids: [uuid!]!) {
|
||||||
@@ -36,69 +35,98 @@ async function RunTheTest() {
|
|||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
for (const [index, job] of jobs.entries()) {
|
const limit = pLimit(5); // Set concurrency limit to 3
|
||||||
process.stdout.cursorTo(0);
|
|
||||||
process.stdout.write(
|
|
||||||
`Processing job ${index + 1} of ${jobs.length}. Failed jobs: ${results.filter((r) => r.result !== "PASS").length}`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
const tasks = jobs.map((job, index) => {
|
||||||
await axios.post(
|
return limit(async () => {
|
||||||
`http://localhost:4000/job/totalsssu`,
|
process.stdout.cursorTo(0);
|
||||||
{ id: job.id },
|
process.stdout.write(
|
||||||
{ headers: { Authorization: bearerToken } }
|
`Processing job ${index + 1} of ${jobs.length}. Failed jobs: ${results.filter((r) => r.overallTotalCorrect !== "PASS").length}. Correct jobs because of adjustment: ${results.filter((r) => r.correctJobsBecauseOfAdjustment).length}`
|
||||||
);
|
);
|
||||||
const { jobs_by_pk: newjob } = await client.request(
|
|
||||||
gql`
|
try {
|
||||||
query GET_JOBS($id: uuid!) {
|
await axios.post(
|
||||||
jobs_by_pk(id: $id) {
|
`http://localhost:4000/job/totalsssu`,
|
||||||
id
|
{ id: job.id },
|
||||||
ro_number
|
{ headers: { Authorization: bearerToken } }
|
||||||
cieca_ttl
|
);
|
||||||
job_totals
|
const { jobs_by_pk: newjob } = await client.request(
|
||||||
ownr_fn
|
gql`
|
||||||
ownr_ln
|
query GET_JOBS($id: uuid!) {
|
||||||
ownr_co_nm
|
jobs_by_pk(id: $id) {
|
||||||
ins_co_nm
|
id
|
||||||
comment
|
ro_number
|
||||||
|
cieca_ttl
|
||||||
|
job_totals
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
ins_co_nm
|
||||||
|
comment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
id: job.id
|
||||||
}
|
}
|
||||||
`,
|
);
|
||||||
{
|
|
||||||
id: job.id
|
const result = {
|
||||||
|
id: newjob.id,
|
||||||
|
owner: `${newjob.ownr_fn} ${newjob.ownr_ln} ${job.ownr_co_nm || ""}`,
|
||||||
|
ins_co: newjob.ins_co_nm,
|
||||||
|
comment: newjob.comment,
|
||||||
|
imexsubtotal: Dinero(newjob.job_totals.totals.subtotal).toFormat("0.00"),
|
||||||
|
imextotalrepair: Dinero(newjob.job_totals.totals.total_repairs).toFormat("0.00"),
|
||||||
|
g_tax: newjob.cieca_ttl.data.g_tax,
|
||||||
|
n_ttl_amt: newjob.cieca_ttl.data.n_ttl_amt,
|
||||||
|
g_ttl_amt: newjob.cieca_ttl.data.g_ttl_amt
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcTotal = newjob.job_totals.totals.total_repairs.amount;
|
||||||
|
const ttlTotal = newjob.cieca_ttl.data.g_ttl_amt * 100;
|
||||||
|
result.difference = (calcTotal - ttlTotal) / 100;
|
||||||
|
|
||||||
|
if (Math.abs(calcTotal - ttlTotal) > 3) {
|
||||||
|
result.overallTotalCorrect = "***FAIL***";
|
||||||
|
} else {
|
||||||
|
result.overallTotalCorrect = "PASS";
|
||||||
}
|
}
|
||||||
);
|
result.ttl_adjustment = Dinero(newjob.job_totals.totals.ttl_adjustment).toFormat();
|
||||||
|
result.ttl_tax_adjustment = Dinero(newjob.job_totals.totals.ttl_tax_adjustment).toFormat();
|
||||||
|
|
||||||
const result = {
|
const calcTaxDinero = Dinero(newjob.job_totals.totals.us_sales_tax_breakdown.ty1Tax)
|
||||||
id: newjob.id,
|
.add(Dinero(newjob.job_totals.totals.us_sales_tax_breakdown.ty2Tax))
|
||||||
owner: `${newjob.ownr_fn} ${newjob.ownr_ln} ${job.ownr_co_nm || ""}`,
|
.add(Dinero(newjob.job_totals.totals.us_sales_tax_breakdown.ty3Tax))
|
||||||
ins_co: newjob.ins_co_nm,
|
.add(Dinero(newjob.job_totals.totals.us_sales_tax_breakdown.ty4Tax))
|
||||||
comment: newjob.comment
|
.add(Dinero(newjob.job_totals.totals.us_sales_tax_breakdown.ty5Tax))
|
||||||
};
|
.add(Dinero(newjob.job_totals.totals.ttl_tax_adjustment));
|
||||||
|
result.calcTax = calcTaxDinero.toFormat("0.00");
|
||||||
|
const calcTax = calcTaxDinero.getAmount() / 100;
|
||||||
|
const emsTax = newjob.cieca_ttl.data.g_tax;
|
||||||
|
result.taxDifference = calcTax - emsTax;
|
||||||
|
|
||||||
const calcTotal = newjob.job_totals.totals.total_repairs.amount;
|
if (Math.abs(calcTax - emsTax) > 3) {
|
||||||
const ttlTotal = newjob.cieca_ttl.data.g_ttl_amt * 100;
|
result.taxCorrect = "***FAIL***";
|
||||||
result.difference = (calcTotal - ttlTotal) / 100;
|
} else {
|
||||||
|
result.taxCorrect = "PASS";
|
||||||
|
}
|
||||||
|
|
||||||
if (Math.abs(calcTotal - ttlTotal) > 3) {
|
results.push(result);
|
||||||
//Diff is greater than 5 cents. Fail it.
|
} catch (error) {
|
||||||
result.result = "***FAIL***";
|
results.push({
|
||||||
} else {
|
ro_number: job.ro_number,
|
||||||
result.result = "PASS";
|
id: job.id,
|
||||||
|
result: error.message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// console.log(`${result.result} => RO ${job.ro_number} - ${job.id} `);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
results.push(result);
|
await Promise.all(tasks);
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
ro_number: job.ro_number,
|
|
||||||
id: job.id,
|
|
||||||
result: "**503 FAILURE**"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.table(results.filter((r) => r.result !== "PASS"));
|
console.table(results.filter((r) => r.overallTotalCorrect !== "PASS"));
|
||||||
|
console.log("=======================================");
|
||||||
const summary = results.reduce(
|
const summary = results.reduce(
|
||||||
(acc, val) => {
|
(acc, val) => {
|
||||||
if (val.result === "PASS") {
|
if (val.result === "PASS") {
|
||||||
@@ -110,18 +138,12 @@ async function RunTheTest() {
|
|||||||
{ pass: 0, fail: 0 }
|
{ pass: 0, fail: 0 }
|
||||||
);
|
);
|
||||||
console.log("Pass Rate: ", ((summary.pass / (summary.fail + summary.pass)) * 100).toFixed(1));
|
console.log("Pass Rate: ", ((summary.pass / (summary.fail + summary.pass)) * 100).toFixed(1));
|
||||||
|
|
||||||
|
const ret = converter.json2csv(results, { emptyFieldValue: "" });
|
||||||
|
|
||||||
|
fs.writeFile(`./logs/totalstest-${Date.now()}.csv`, ret, (error) => console.log(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
RunTheTest();
|
RunTheTest().catch((error) => {
|
||||||
|
console.log("Error in RunTheTest: ", error);
|
||||||
// mutation {
|
});
|
||||||
// delete_jobs(where: {shopid: {_eq: "a7ee1503-ee05-4a02-b80e-bdb11d1cc8ac"}}) {
|
|
||||||
// affected_rows
|
|
||||||
// }
|
|
||||||
// delete_owners(where: {shopid: {_eq: "a7ee1503-ee05-4a02-b80e-bdb11d1cc8ac"}}) {
|
|
||||||
// affected_rows
|
|
||||||
// }
|
|
||||||
// delete_vehicles(where: {shopid: {_eq: "a7ee1503-ee05-4a02-b80e-bdb11d1cc8ac"}}) {
|
|
||||||
// affected_rows
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
3437
package-lock.json
generated
3437
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -19,13 +19,13 @@
|
|||||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.679.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.693.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.675.0",
|
"@aws-sdk/client-elasticache": "^3.693.0",
|
||||||
"@aws-sdk/client-s3": "^3.689.0",
|
"@aws-sdk/client-s3": "^3.693.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.675.0",
|
"@aws-sdk/client-secrets-manager": "^3.693.0",
|
||||||
"@aws-sdk/client-ses": "^3.675.0",
|
"@aws-sdk/client-ses": "^3.693.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.675.0",
|
"@aws-sdk/credential-provider-node": "^3.693.0",
|
||||||
"@opensearch-project/opensearch": "^2.12.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
@@ -34,20 +34,21 @@
|
|||||||
"bluebird": "^3.7.2",
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"canvas": "^2.11.2",
|
"canvas": "^2.11.2",
|
||||||
"chart.js": "^4.4.5",
|
"chart.js": "^4.4.6",
|
||||||
"cloudinary": "^2.5.1",
|
"cloudinary": "^2.5.1",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.5",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"csrf": "^3.1.0",
|
"csrf": "^3.1.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"firebase-admin": "^12.6.0",
|
"firebase-admin": "^13.0.0",
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"inline-css": "^4.0.2",
|
"inline-css": "^4.0.2",
|
||||||
"intuit-oauth": "^4.1.2",
|
"intuit-oauth": "^4.1.3",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
"json-2-csv": "^5.5.6",
|
"json-2-csv": "^5.5.6",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -56,18 +57,18 @@
|
|||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-mailjet": "^6.0.6",
|
"node-mailjet": "^6.0.6",
|
||||||
"node-persist": "^4.0.3",
|
"node-persist": "^4.0.3",
|
||||||
"nodemailer": "^6.9.15",
|
"nodemailer": "^6.9.16",
|
||||||
"phone": "^3.1.51",
|
"phone": "^3.1.53",
|
||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"soap": "^1.1.5",
|
"soap": "^1.1.6",
|
||||||
"socket.io": "^4.8.0",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^10.0.3",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^4.23.0",
|
"twilio": "^4.23.0",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"winston": "^3.15.0",
|
"winston": "^3.17.0",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
@@ -79,6 +80,7 @@
|
|||||||
"eslint": "^9.15.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.12.0",
|
||||||
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"source-map-explorer": "^2.5.2"
|
"source-map-explorer": "^2.5.2"
|
||||||
}
|
}
|
||||||
|
|||||||
42
server.js
42
server.js
@@ -22,7 +22,10 @@ const { applyIOHelpers } = require("./server/utils/ioHelpers");
|
|||||||
const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents");
|
const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents");
|
||||||
const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/client-elasticache");
|
const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/client-elasticache");
|
||||||
const { InstanceRegion } = require("./server/utils/instanceMgr");
|
const { InstanceRegion } = require("./server/utils/instanceMgr");
|
||||||
|
const StartStatusReporter = require("./server/utils/statusReporter");
|
||||||
|
|
||||||
|
const cleanupTasks = [];
|
||||||
|
let isShuttingDown = false;
|
||||||
const CLUSTER_RETRY_BASE_DELAY = 100;
|
const CLUSTER_RETRY_BASE_DELAY = 100;
|
||||||
const CLUSTER_RETRY_MAX_DELAY = 5000;
|
const CLUSTER_RETRY_MAX_DELAY = 5000;
|
||||||
const CLUSTER_RETRY_JITTER = 100;
|
const CLUSTER_RETRY_JITTER = 100;
|
||||||
@@ -293,6 +296,15 @@ const main = async () => {
|
|||||||
applyRoutes({ app });
|
applyRoutes({ app });
|
||||||
redisSocketEvents({ io: ioRedis, redisHelpers, ioHelpers, logger });
|
redisSocketEvents({ io: ioRedis, redisHelpers, ioHelpers, logger });
|
||||||
|
|
||||||
|
const StatusReporter = StartStatusReporter();
|
||||||
|
registerCleanupTask(async () => {
|
||||||
|
StatusReporter.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add SIGTERM signal handler
|
||||||
|
process.on("SIGTERM", handleSigterm);
|
||||||
|
process.on("SIGINT", handleSigterm); // Optional: Handle Ctrl+C
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await server.listen(port);
|
await server.listen(port);
|
||||||
logger.log(`Server started on port ${port}`, "INFO", "api");
|
logger.log(`Server started on port ${port}`, "INFO", "api");
|
||||||
@@ -310,3 +322,33 @@ main().catch((error) => {
|
|||||||
// Note: If we want the app to crash on all uncaught async operations, we would
|
// Note: If we want the app to crash on all uncaught async operations, we would
|
||||||
// need to put a `process.exit(1);` here
|
// need to put a `process.exit(1);` here
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register a cleanup task
|
||||||
|
function registerCleanupTask(task) {
|
||||||
|
cleanupTasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIGTERM handler
|
||||||
|
async function handleSigterm() {
|
||||||
|
if (isShuttingDown) {
|
||||||
|
logger.log("sigterm-api", "WARN", null, null, { message: "Shutdown already in progress, ignoring signal." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isShuttingDown = true;
|
||||||
|
|
||||||
|
logger.log("sigterm-api", "WARN", null, null, { message: "SIGTERM Received. Starting graceful shutdown." });
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const task of cleanupTasks) {
|
||||||
|
logger.log("sigterm-api", "WARN", null, null, { message: `Running cleanup task: ${task.name}` });
|
||||||
|
|
||||||
|
await task();
|
||||||
|
}
|
||||||
|
logger.log("sigterm-api", "WARN", null, null, { message: `All cleanup tasks completed.` });
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("sigterm-api-error", "ERROR", null, null, { message: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -547,6 +547,61 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (jobs_by_pk.job_totals.totals?.ttl_adjustment) {
|
||||||
|
// Do not need to check for ImEX or Rome because ImEX uses a different totals calculation that will never set this field.
|
||||||
|
if (qbo) {
|
||||||
|
const taxAccountCode = findTaxCode(
|
||||||
|
{
|
||||||
|
local: false,
|
||||||
|
federal: InstanceManager({ imex: true, rome: false }),
|
||||||
|
state: jobs_by_pk.tax_lbr_rt === 0 ? false : true
|
||||||
|
},
|
||||||
|
bodyshop.md_responsibility_centers.sales_tax_codes
|
||||||
|
);
|
||||||
|
|
||||||
|
const QboTaxId = InstanceManager({
|
||||||
|
imex: taxCodes[taxAccountCode],
|
||||||
|
rome: CheckQBOUSATaxID({
|
||||||
|
// jobline: jobline,
|
||||||
|
type: "adjustment",
|
||||||
|
job: jobs_by_pk
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
InvoiceLineAdd.push({
|
||||||
|
DetailType: "SalesItemLineDetail",
|
||||||
|
Amount: Dinero(jobs_by_pk.job_totals.totals?.ttl_adjustment).toFormat(DineroQbFormat),
|
||||||
|
SalesItemLineDetail: {
|
||||||
|
...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}),
|
||||||
|
ItemRef: {
|
||||||
|
value: items[responsibilityCenters.ttl_adjustment?.accountitem]
|
||||||
|
},
|
||||||
|
TaxCodeRef: {
|
||||||
|
value: QboTaxId
|
||||||
|
},
|
||||||
|
Qty: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
InvoiceLineAdd.push({
|
||||||
|
ItemRef: {
|
||||||
|
FullName: responsibilityCenters.ttl_adjustment?.accountitem
|
||||||
|
},
|
||||||
|
Desc: "Adjustment",
|
||||||
|
Quantity: 1,
|
||||||
|
Amount: Dinero(jobs_by_pk.job_totals.totals?.ttl_adjustment).toFormat(DineroQbFormat),
|
||||||
|
SalesTaxCodeRef: InstanceManager({
|
||||||
|
imex: {
|
||||||
|
FullName: "E"
|
||||||
|
},
|
||||||
|
rome: {
|
||||||
|
FullName: bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Add tax lines
|
//Add tax lines
|
||||||
const job_totals = jobs_by_pk.job_totals;
|
const job_totals = jobs_by_pk.job_totals;
|
||||||
const federal_tax = Dinero(job_totals.totals.federal_tax);
|
const federal_tax = Dinero(job_totals.totals.federal_tax);
|
||||||
@@ -823,7 +878,60 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (jobs_by_pk.job_totals.totals.ttl_tax_adjustment) {
|
||||||
|
// Do not need to check for ImEX or Rome because ImEX uses a different totals calculation that will never set this field.
|
||||||
|
if (qbo) {
|
||||||
|
const taxAccountCode = findTaxCode(
|
||||||
|
{
|
||||||
|
local: false,
|
||||||
|
federal: InstanceManager({ imex: true, rome: false }),
|
||||||
|
state: jobs_by_pk.tax_lbr_rt === 0 ? false : true
|
||||||
|
},
|
||||||
|
bodyshop.md_responsibility_centers.sales_tax_codes
|
||||||
|
);
|
||||||
|
|
||||||
|
const QboTaxId = InstanceManager({
|
||||||
|
imex: taxCodes[taxAccountCode],
|
||||||
|
rome: CheckQBOUSATaxID({
|
||||||
|
// jobline: jobline,
|
||||||
|
type: "adjustment",
|
||||||
|
job: jobs_by_pk
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
InvoiceLineAdd.push({
|
||||||
|
DetailType: "SalesItemLineDetail",
|
||||||
|
Amount: Dinero(jobs_by_pk.job_totals.totals?.ttl_tax_adjustment).toFormat(DineroQbFormat),
|
||||||
|
SalesItemLineDetail: {
|
||||||
|
...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}),
|
||||||
|
ItemRef: {
|
||||||
|
value: items[responsibilityCenters.ttl_tax_adjustment?.accountitem]
|
||||||
|
},
|
||||||
|
TaxCodeRef: {
|
||||||
|
value: QboTaxId
|
||||||
|
},
|
||||||
|
Qty: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
InvoiceLineAdd.push({
|
||||||
|
ItemRef: {
|
||||||
|
FullName: responsibilityCenters.ttl_tax_adjustment?.accountitem
|
||||||
|
},
|
||||||
|
Desc: "Tax Adjustment",
|
||||||
|
Quantity: 1,
|
||||||
|
Amount: Dinero(jobs_by_pk.job_totals.totals?.ttl_tax_adjustment).toFormat(DineroQbFormat),
|
||||||
|
SalesTaxCodeRef: InstanceManager({
|
||||||
|
imex: {
|
||||||
|
FullName: "E"
|
||||||
|
},
|
||||||
|
rome: {
|
||||||
|
FullName: bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!qbo && InvoiceLineAdd.length === 0) {
|
if (!qbo && InvoiceLineAdd.length === 0) {
|
||||||
//Handle the scenario where there is a $0 sale invoice.
|
//Handle the scenario where there is a $0 sale invoice.
|
||||||
InvoiceLineAdd.push({
|
InvoiceLineAdd.push({
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ function calculateAllocations(connectionData, job) {
|
|||||||
// console.log("NO MASH ACCOUNT FOUND!!");
|
// console.log("NO MASH ACCOUNT FOUND!!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (InstanceManager({ rome: true })) {
|
if (InstanceManager({ rome: true })) {
|
||||||
//profile level adjustments for parts
|
//profile level adjustments for parts
|
||||||
Object.keys(job.job_totals.parts.adjustments).forEach((key) => {
|
Object.keys(job.job_totals.parts.adjustments).forEach((key) => {
|
||||||
@@ -426,6 +427,41 @@ function calculateAllocations(connectionData, job) {
|
|||||||
} else {
|
} else {
|
||||||
return { ...taxAllocations[key], tax: key };
|
return { ...taxAllocations[key], tax: key };
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
...(job.job_totals.totals.ttl_adjustment
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
center: "SUB ADJ",
|
||||||
|
sale: Dinero(job.job_totals.totals.ttl_adjustment),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: {
|
||||||
|
name: "SUB ADJ",
|
||||||
|
accountdesc: "SUB ADJ",
|
||||||
|
accountitem: "SUB ADJ",
|
||||||
|
accountname: "SUB ADJ",
|
||||||
|
dms_acctnumber: bodyshop.md_responsibility_centers.ttl_adjustment.dms_acctnumber
|
||||||
|
},
|
||||||
|
costCenter: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(job.job_totals.totals.ttl_tax_adjustment
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
center: "TAX ADJ",
|
||||||
|
sale: Dinero(job.job_totals.totals.ttl_tax_adjustment),
|
||||||
|
cost: Dinero(),
|
||||||
|
profitCenter: {
|
||||||
|
name: "TAX ADJ",
|
||||||
|
accountdesc: "TAX ADJ",
|
||||||
|
accountitem: "TAX ADJ",
|
||||||
|
accountname: "TAX ADJ",
|
||||||
|
dms_acctnumber: bodyshop.md_responsibility_centers.ttl_tax_adjustment.dms_acctnumber
|
||||||
|
},
|
||||||
|
costCenter: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ const ftpSetup = {
|
|||||||
port: process.env.AUTOHOUSE_PORT,
|
port: process.env.AUTOHOUSE_PORT,
|
||||||
username: process.env.AUTOHOUSE_USER,
|
username: process.env.AUTOHOUSE_USER,
|
||||||
password: process.env.AUTOHOUSE_PASSWORD,
|
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: {
|
algorithms: {
|
||||||
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const allxmlsToUpload = [];
|
|
||||||
const allErrors = [];
|
|
||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
// Only process if in production environment.
|
// Only process if in production environment.
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
@@ -55,12 +55,13 @@ exports.default = async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.log("autohouse-start", "DEBUG", "api", null, null);
|
logger.log("autohouse-start", "DEBUG", "api", null, null);
|
||||||
|
const allXMLResults = [];
|
||||||
|
const allErrors = [];
|
||||||
|
|
||||||
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); //Query for the List of Bodyshop Clients.
|
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); //Query for the List of Bodyshop Clients.
|
||||||
const specificShopIds = req.body.bodyshopIds; // ['uuid];
|
const specificShopIds = req.body.bodyshopIds; // ['uuid];
|
||||||
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
||||||
|
|
||||||
const batchSize = 10;
|
|
||||||
|
|
||||||
const shopsToProcess =
|
const shopsToProcess =
|
||||||
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||||
logger.log("autohouse-shopsToProcess-generated", "DEBUG", "api", null, null);
|
logger.log("autohouse-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||||
@@ -69,27 +70,18 @@ exports.default = async (req, res) => {
|
|||||||
logger.log("autohouse-shopsToProcess-empty", "DEBUG", "api", null, null);
|
logger.log("autohouse-shopsToProcess-empty", "DEBUG", "api", null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const batchPromises = [];
|
|
||||||
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
|
|
||||||
const batch = shopsToProcess.slice(i, i + batchSize);
|
|
||||||
const batchPromise = (async () => {
|
|
||||||
await processBatch(batch, start, end);
|
|
||||||
|
|
||||||
if (skipUpload) {
|
await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);
|
||||||
for (const xmlObj of allxmlsToUpload) {
|
|
||||||
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await uploadViaSFTP(allxmlsToUpload);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
batchPromises.push(batchPromise);
|
|
||||||
}
|
|
||||||
await Promise.all(batchPromises);
|
|
||||||
await sendServerEmail({
|
await sendServerEmail({
|
||||||
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
|
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
|
||||||
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||||
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
|
allXMLResults.map((x) => ({
|
||||||
|
imexshopid: x.imexshopid,
|
||||||
|
filename: x.filename,
|
||||||
|
count: x.count,
|
||||||
|
result: x.result
|
||||||
|
})),
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
)}`
|
)}`
|
||||||
@@ -101,8 +93,8 @@ exports.default = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function processBatch(batch, start, end) {
|
async function processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
|
||||||
for (const bodyshop of batch) {
|
for (const bodyshop of shopsToProcess) {
|
||||||
const erroredJobs = [];
|
const erroredJobs = [];
|
||||||
try {
|
try {
|
||||||
logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
@@ -132,12 +124,27 @@ async function processBatch(batch, start, end) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ret = builder.create({}, autoHouseObject).end({ allowEmptyTags: true });
|
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
|
||||||
|
};
|
||||||
|
|
||||||
allxmlsToUpload.push({
|
if (skipUpload) {
|
||||||
count: autoHouseObject.AutoHouseExport.RepairOrder.length,
|
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
||||||
xml: ret,
|
} else {
|
||||||
filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml`
|
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, {
|
logger.log("autohouse-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
@@ -169,33 +176,35 @@ async function processBatch(batch, start, end) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadViaSFTP(allxmlsToUpload) {
|
async function uploadViaSFTP(xmlObj) {
|
||||||
const sftp = new Client();
|
const sftp = new Client();
|
||||||
sftp.on("error", (errors) =>
|
sftp.on("error", (errors) =>
|
||||||
logger.log("autohouse-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
|
logger.log("autohouse-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||||
|
error: errors.message,
|
||||||
|
stack: errors.stack
|
||||||
|
})
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
//Connect to the FTP and upload all.
|
//Connect to the FTP and upload all.
|
||||||
await sftp.connect(ftpSetup);
|
await sftp.connect(ftpSetup);
|
||||||
|
|
||||||
for (const xmlObj of allxmlsToUpload) {
|
try {
|
||||||
try {
|
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
|
||||||
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
|
logger.log("autohouse-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
|
||||||
logger.log("autohouse-sftp-upload", "DEBUG", "api", null, {
|
imexshopid: xmlObj.imexshopid,
|
||||||
filename: xmlObj.filename,
|
filename: xmlObj.filename,
|
||||||
result: xmlObj.result
|
result: xmlObj.result
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("autohouse-sftp-upload-error", "ERROR", "api", null, {
|
logger.log("autohouse-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||||
filename: xmlObj.filename,
|
filename: xmlObj.filename,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("autohouse-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
logger.log("autohouse-sftp-error", "ERROR", "api", xmlObj.bodyshopid, { error: error.message, stack: error.stack });
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
sftp.end();
|
sftp.end();
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ const ftpSetup = {
|
|||||||
port: process.env.CHATTER_PORT,
|
port: process.env.CHATTER_PORT,
|
||||||
username: process.env.CHATTER_USER,
|
username: process.env.CHATTER_USER,
|
||||||
privateKey: null,
|
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: {
|
algorithms: {
|
||||||
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const allcsvsToUpload = [];
|
|
||||||
const allErrors = [];
|
|
||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
// Only process if in production environment.
|
// Only process if in production environment.
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
@@ -47,12 +47,13 @@ exports.default = async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.log("chatter-start", "DEBUG", "api", null, null);
|
logger.log("chatter-start", "DEBUG", "api", null, null);
|
||||||
|
const allChatterObjects = [];
|
||||||
|
const allErrors = [];
|
||||||
|
|
||||||
const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); //Query for the List of Bodyshop Clients.
|
const { bodyshops } = await client.request(queries.GET_CHATTER_SHOPS); //Query for the List of Bodyshop Clients.
|
||||||
const specificShopIds = req.body.bodyshopIds; // ['uuid];
|
const specificShopIds = req.body.bodyshopIds; // ['uuid];
|
||||||
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
||||||
|
|
||||||
const batchSize = 10;
|
|
||||||
|
|
||||||
const shopsToProcess =
|
const shopsToProcess =
|
||||||
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||||
logger.log("chatter-shopsToProcess-generated", "DEBUG", "api", null, null);
|
logger.log("chatter-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||||
@@ -62,29 +63,24 @@ exports.default = async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const batchPromises = [];
|
await processBatch(shopsToProcess, start, end, allChatterObjects, allErrors);
|
||||||
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
|
|
||||||
const batch = shopsToProcess.slice(i, i + batchSize);
|
const csvToUpload = {
|
||||||
const batchPromise = (async () => {
|
count: allChatterObjects.length,
|
||||||
await processBatch(batch, start, end);
|
csv: converter.json2csv(allChatterObjects, { emptyFieldValue: "" }),
|
||||||
if (skipUpload) {
|
filename: `IMEX_ONLINE_solicitation_${moment().format("YYYYMMDD")}.csv`
|
||||||
for (const csvObj of allcsvsToUpload) {
|
};
|
||||||
await fs.promises.writeFile(`./logs/${csvObj.filename}`, csvObj.csv);
|
|
||||||
}
|
if (skipUpload) {
|
||||||
} else {
|
await fs.promises.writeFile(`./logs/${csvToUpload.filename}`, csvToUpload.csv);
|
||||||
await uploadViaSFTP(allcsvsToUpload);
|
} else {
|
||||||
}
|
await uploadViaSFTP(csvToUpload);
|
||||||
})();
|
|
||||||
batchPromises.push(batchPromise);
|
|
||||||
}
|
}
|
||||||
await Promise.all(batchPromises);
|
|
||||||
await sendServerEmail({
|
await sendServerEmail({
|
||||||
subject: `Chatter Report ${moment().format("MM-DD-YY")}`,
|
subject: `Chatter Report ${moment().format("MM-DD-YY")}`,
|
||||||
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\n
|
||||||
allcsvsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
|
Uploaded:\n${JSON.stringify({ filename: csvToUpload.filename, count: csvToUpload.count, result: csvToUpload.result }, null, 2)}`
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}`
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log("chatter-end", "DEBUG", "api", null, null);
|
logger.log("chatter-end", "DEBUG", "api", null, null);
|
||||||
@@ -93,8 +89,8 @@ exports.default = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function processBatch(batch, start, end) {
|
async function processBatch(shopsToProcess, start, end, allChatterObjects, allErrors) {
|
||||||
for (const bodyshop of batch) {
|
for (const bodyshop of shopsToProcess) {
|
||||||
try {
|
try {
|
||||||
logger.log("chatter-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
logger.log("chatter-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
shopname: bodyshop.shopname
|
shopname: bodyshop.shopname
|
||||||
@@ -113,18 +109,11 @@ async function processBatch(batch, start, end) {
|
|||||||
lastname: j.ownr_co_nm ? j.ownr_co_nm : j.ownr_ln,
|
lastname: j.ownr_co_nm ? j.ownr_co_nm : j.ownr_ln,
|
||||||
transaction_id: j.ro_number,
|
transaction_id: j.ro_number,
|
||||||
email: j.ownr_ea,
|
email: j.ownr_ea,
|
||||||
phone_number: j.ownr_ph1
|
phone_number: j.ownr_ph1,
|
||||||
|
transaction_time: (j.actual_delivery && moment(j.actual_delivery).tz(bodyshop.timezone).format("YYYYMMDD-HHmm")) || ""
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
allChatterObjects.push(...chatterObject);
|
||||||
const ret = converter.json2csv(chatterObject, { emptyFieldValue: "" });
|
|
||||||
|
|
||||||
allcsvsToUpload.push({
|
|
||||||
count: chatterObject.length,
|
|
||||||
csv: ret,
|
|
||||||
filename: `${bodyshop.shopname}_solicitation_${moment().format("YYYYMMDD")}.csv`
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log("chatter-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
logger.log("chatter-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
shopname: bodyshop.shopname
|
shopname: bodyshop.shopname
|
||||||
});
|
});
|
||||||
@@ -166,7 +155,7 @@ async function getPrivateKey() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadViaSFTP(allcsvsToUpload) {
|
async function uploadViaSFTP(csvToUpload) {
|
||||||
const sftp = new Client();
|
const sftp = new Client();
|
||||||
sftp.on("error", (errors) =>
|
sftp.on("error", (errors) =>
|
||||||
logger.log("chatter-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
|
logger.log("chatter-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
|
||||||
@@ -178,21 +167,19 @@ async function uploadViaSFTP(allcsvsToUpload) {
|
|||||||
//Connect to the FTP and upload all.
|
//Connect to the FTP and upload all.
|
||||||
await sftp.connect({ ...ftpSetup, privateKey });
|
await sftp.connect({ ...ftpSetup, privateKey });
|
||||||
|
|
||||||
for (const csvObj of allcsvsToUpload) {
|
try {
|
||||||
try {
|
csvToUpload.result = await sftp.put(Buffer.from(csvToUpload.csv), `${csvToUpload.filename}`);
|
||||||
csvObj.result = await sftp.put(Buffer.from(csvObj.csv), `${csvObj.filename}`);
|
logger.log("chatter-sftp-upload", "DEBUG", "api", null, {
|
||||||
logger.log("chatter-sftp-upload", "DEBUG", "api", null, {
|
filename: csvToUpload.filename,
|
||||||
filename: csvObj.filename,
|
result: csvToUpload.result
|
||||||
result: csvObj.result
|
});
|
||||||
});
|
} catch (error) {
|
||||||
} catch (error) {
|
logger.log("chatter-sftp-upload-error", "ERROR", "api", null, {
|
||||||
logger.log("chatter-sftp-upload-error", "ERROR", "api", null, {
|
filename: csvToUpload.filename,
|
||||||
filename: csvObj.filename,
|
error: csvToUpload.message,
|
||||||
error: error.message,
|
stack: csvToUpload.stack
|
||||||
stack: error.stack
|
});
|
||||||
});
|
throw error;
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("chatter-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
logger.log("chatter-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ const ftpSetup = {
|
|||||||
port: process.env.CLAIMSCORP_PORT,
|
port: process.env.CLAIMSCORP_PORT,
|
||||||
username: process.env.CLAIMSCORP_USER,
|
username: process.env.CLAIMSCORP_USER,
|
||||||
password: process.env.CLAIMSCORP_PASSWORD,
|
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: {
|
algorithms: {
|
||||||
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const allxmlsToUpload = [];
|
|
||||||
const allErrors = [];
|
|
||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
// Only process if in production environment.
|
// Only process if in production environment.
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
@@ -54,12 +54,13 @@ exports.default = async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.log("claimscorp-start", "DEBUG", "api", null, null);
|
logger.log("claimscorp-start", "DEBUG", "api", null, null);
|
||||||
|
const allXMLResults = [];
|
||||||
|
const allErrors = [];
|
||||||
|
|
||||||
const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS); //Query for the List of Bodyshop Clients.
|
const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS); //Query for the List of Bodyshop Clients.
|
||||||
const specificShopIds = req.body.bodyshopIds; // ['uuid];
|
const specificShopIds = req.body.bodyshopIds; // ['uuid];
|
||||||
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
||||||
|
|
||||||
const batchSize = 10;
|
|
||||||
|
|
||||||
const shopsToProcess =
|
const shopsToProcess =
|
||||||
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||||
logger.log("claimscorp-shopsToProcess-generated", "DEBUG", "api", null, null);
|
logger.log("claimscorp-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||||
@@ -68,27 +69,18 @@ exports.default = async (req, res) => {
|
|||||||
logger.log("claimscorp-shopsToProcess-empty", "DEBUG", "api", null, null);
|
logger.log("claimscorp-shopsToProcess-empty", "DEBUG", "api", null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const batchPromises = [];
|
|
||||||
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
|
|
||||||
const batch = shopsToProcess.slice(i, i + batchSize);
|
|
||||||
const batchPromise = (async () => {
|
|
||||||
await processBatch(batch, start, end);
|
|
||||||
|
|
||||||
if (skipUpload) {
|
await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);
|
||||||
for (const xmlObj of allxmlsToUpload) {
|
|
||||||
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await uploadViaSFTP(allxmlsToUpload);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
batchPromises.push(batchPromise);
|
|
||||||
}
|
|
||||||
await Promise.all(batchPromises);
|
|
||||||
await sendServerEmail({
|
await sendServerEmail({
|
||||||
subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`,
|
subject: `ClaimsCorp Report ${moment().format("MM-DD-YY")}`,
|
||||||
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||||
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
|
allXMLResults.map((x) => ({
|
||||||
|
imexshopid: x.imexshopid,
|
||||||
|
filename: x.filename,
|
||||||
|
count: x.count,
|
||||||
|
result: x.result
|
||||||
|
})),
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
)}`
|
)}`
|
||||||
@@ -100,8 +92,8 @@ exports.default = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function processBatch(batch, start, end) {
|
async function processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
|
||||||
for (const bodyshop of batch) {
|
for (const bodyshop of shopsToProcess) {
|
||||||
const erroredJobs = [];
|
const erroredJobs = [];
|
||||||
try {
|
try {
|
||||||
logger.log("claimscorp-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
logger.log("claimscorp-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
@@ -135,12 +127,27 @@ async function processBatch(batch, start, end) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ret = builder.create({}, claimsCorpObject).end({ allowEmptyTags: true });
|
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
|
||||||
|
};
|
||||||
|
|
||||||
allxmlsToUpload.push({
|
if (skipUpload) {
|
||||||
count: claimsCorpObject.DataFeed.ShopInfo.RO.length,
|
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
||||||
xml: ret,
|
} else {
|
||||||
filename: `${bodyshop.claimscorpid}-${moment().format("YYYYMMDDTHHMMss")}.xml`
|
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, {
|
logger.log("claimscorp-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
@@ -172,33 +179,38 @@ async function processBatch(batch, start, end) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadViaSFTP(allxmlsToUpload) {
|
async function uploadViaSFTP(xmlObj) {
|
||||||
const sftp = new Client();
|
const sftp = new Client();
|
||||||
sftp.on("error", (errors) =>
|
sftp.on("error", (errors) =>
|
||||||
logger.log("claimscorp-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
|
logger.log("claimscorp-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||||
|
error: errors.message,
|
||||||
|
stack: errors.stack
|
||||||
|
})
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
//Connect to the FTP and upload all.
|
//Connect to the FTP and upload all.
|
||||||
await sftp.connect(ftpSetup);
|
await sftp.connect(ftpSetup);
|
||||||
|
|
||||||
for (const xmlObj of allxmlsToUpload) {
|
try {
|
||||||
try {
|
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
|
||||||
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
|
logger.log("claimscorp-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
|
||||||
logger.log("claimscorp-sftp-upload", "DEBUG", "api", null, {
|
imexshopid: xmlObj.imexshopid,
|
||||||
filename: xmlObj.filename,
|
filename: xmlObj.filename,
|
||||||
result: xmlObj.result
|
result: xmlObj.result
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("claimscorp-sftp-upload-error", "ERROR", "api", null, {
|
logger.log("claimscorp-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||||
filename: xmlObj.filename,
|
filename: xmlObj.filename,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("claimscorp-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
logger.log("claimscorp-sftp-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
sftp.end();
|
sftp.end();
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ const ftpSetup = {
|
|||||||
port: process.env.KAIZEN_PORT,
|
port: process.env.KAIZEN_PORT,
|
||||||
username: process.env.KAIZEN_USER,
|
username: process.env.KAIZEN_USER,
|
||||||
password: process.env.KAIZEN_PASSWORD,
|
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: {
|
algorithms: {
|
||||||
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const allxmlsToUpload = [];
|
|
||||||
const allErrors = [];
|
|
||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
// Only process if in production environment.
|
// Only process if in production environment.
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
@@ -53,12 +53,13 @@ exports.default = async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.log("kaizen-start", "DEBUG", "api", null, null);
|
logger.log("kaizen-start", "DEBUG", "api", null, null);
|
||||||
|
const allXMLResults = [];
|
||||||
|
const allErrors = [];
|
||||||
|
|
||||||
const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { imexshopid: kaizenShopsIDs }); //Query for the List of Bodyshop Clients.
|
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 specificShopIds = req.body.bodyshopIds; // ['uuid];
|
||||||
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
||||||
|
|
||||||
const batchSize = 10;
|
|
||||||
|
|
||||||
const shopsToProcess =
|
const shopsToProcess =
|
||||||
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||||
logger.log("kaizen-shopsToProcess-generated", "DEBUG", "api", null, null);
|
logger.log("kaizen-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||||
@@ -67,27 +68,18 @@ exports.default = async (req, res) => {
|
|||||||
logger.log("kaizen-shopsToProcess-empty", "DEBUG", "api", null, null);
|
logger.log("kaizen-shopsToProcess-empty", "DEBUG", "api", null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const batchPromises = [];
|
|
||||||
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
|
|
||||||
const batch = shopsToProcess.slice(i, i + batchSize);
|
|
||||||
const batchPromise = (async () => {
|
|
||||||
await processBatch(batch, start, end);
|
|
||||||
|
|
||||||
if (skipUpload) {
|
await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);
|
||||||
for (const xmlObj of allxmlsToUpload) {
|
|
||||||
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await uploadViaSFTP(allxmlsToUpload);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
batchPromises.push(batchPromise);
|
|
||||||
}
|
|
||||||
await Promise.all(batchPromises);
|
|
||||||
await sendServerEmail({
|
await sendServerEmail({
|
||||||
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
|
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
|
||||||
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||||
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
|
allXMLResults.map((x) => ({
|
||||||
|
imexshopid: x.imexshopid,
|
||||||
|
filename: x.filename,
|
||||||
|
count: x.count,
|
||||||
|
result: x.result
|
||||||
|
})),
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
)}`
|
)}`
|
||||||
@@ -99,8 +91,8 @@ exports.default = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function processBatch(batch, start, end) {
|
async function processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
|
||||||
for (const bodyshop of batch) {
|
for (const bodyshop of shopsToProcess) {
|
||||||
const erroredJobs = [];
|
const erroredJobs = [];
|
||||||
try {
|
try {
|
||||||
logger.log("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
logger.log("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
@@ -133,12 +125,26 @@ async function processBatch(batch, start, end) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ret = builder.create({}, kaizenObject).end({ allowEmptyTags: true });
|
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
|
||||||
|
};
|
||||||
|
|
||||||
allxmlsToUpload.push({
|
if (skipUpload) {
|
||||||
count: kaizenObject.DataFeed.ShopInfo.Jobs.length,
|
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
||||||
xml: ret,
|
} else {
|
||||||
filename: `${bodyshop.shopname}-${moment().format("YYYYMMDDTHHMMss")}.xml`
|
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, {
|
logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
@@ -170,33 +176,35 @@ async function processBatch(batch, start, end) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadViaSFTP(allxmlsToUpload) {
|
async function uploadViaSFTP(xmlObj) {
|
||||||
const sftp = new Client();
|
const sftp = new Client();
|
||||||
sftp.on("error", (errors) =>
|
sftp.on("error", (errors) =>
|
||||||
logger.log("kaizen-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
|
logger.log("kaizen-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||||
|
error: errors.message,
|
||||||
|
stack: errors.stack
|
||||||
|
})
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
//Connect to the FTP and upload all.
|
//Connect to the FTP and upload all.
|
||||||
await sftp.connect(ftpSetup);
|
await sftp.connect(ftpSetup);
|
||||||
|
|
||||||
for (const xmlObj of allxmlsToUpload) {
|
try {
|
||||||
try {
|
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
|
||||||
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
|
logger.log("kaizen-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
|
||||||
logger.log("kaizen-sftp-upload", "DEBUG", "api", null, {
|
imexshopid: xmlObj.imexshopid,
|
||||||
filename: xmlObj.filename,
|
filename: xmlObj.filename,
|
||||||
result: xmlObj.result
|
result: xmlObj.result
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("kaizen-sftp-upload-error", "ERROR", "api", null, {
|
logger.log("kaizen-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
|
||||||
filename: xmlObj.filename,
|
filename: xmlObj.filename,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("kaizen-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
logger.log("kaizen-sftp-error", "ERROR", "api", xmlObj.bodyshopid, { error: error.message, stack: error.stack });
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
sftp.end();
|
sftp.end();
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ const sendNotification = async (req, res) => {
|
|||||||
admin
|
admin
|
||||||
.messaging()
|
.messaging()
|
||||||
.send({
|
.send({
|
||||||
topic: "PRD_PATRICK-messaging",
|
topic: req.body.topic,
|
||||||
notification: {
|
notification: {
|
||||||
title: `ImEX Online Message - `,
|
title: `ImEX Online Message - `,
|
||||||
body: "Test Noti."
|
body: "Test Noti."
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ query FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID($mssid: String!, $phone: String!) {
|
|||||||
id
|
id
|
||||||
conversations(where: { phone_num: { _eq: $phone } }) {
|
conversations(where: { phone_num: { _eq: $phone } }) {
|
||||||
id
|
id
|
||||||
|
created_at
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
@@ -87,6 +88,21 @@ mutation RECEIVE_MESSAGE($msg: [messages_insert_input!]!) {
|
|||||||
updated_at
|
updated_at
|
||||||
unreadcnt
|
unreadcnt
|
||||||
phone_num
|
phone_num
|
||||||
|
label
|
||||||
|
job_conversations {
|
||||||
|
job {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages_aggregate (where: { read: { _eq: false }, isoutbound: { _eq: false } }){
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
conversationid
|
conversationid
|
||||||
created_at
|
created_at
|
||||||
@@ -116,6 +132,7 @@ mutation INSERT_MESSAGE($msg: [messages_insert_input!]!, $conversationid: uuid!)
|
|||||||
id
|
id
|
||||||
archived
|
archived
|
||||||
bodyshop {
|
bodyshop {
|
||||||
|
id
|
||||||
imexshopid
|
imexshopid
|
||||||
}
|
}
|
||||||
created_at
|
created_at
|
||||||
@@ -144,6 +161,11 @@ mutation UPDATE_MESSAGE($msid: String!, $fields: messages_set_input!) {
|
|||||||
update_messages(where: { msid: { _eq: $msid } }, _set: $fields) {
|
update_messages(where: { msid: { _eq: $msid } }, _set: $fields) {
|
||||||
returning {
|
returning {
|
||||||
id
|
id
|
||||||
|
status
|
||||||
|
conversationid
|
||||||
|
conversation{
|
||||||
|
bodyshopid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
@@ -840,6 +862,7 @@ exports.CHATTER_QUERY = `query CHATTER_EXPORT($start: timestamptz, $bodyshopid:
|
|||||||
timezone
|
timezone
|
||||||
}
|
}
|
||||||
jobs(where: {_and: [{converted: {_eq: true}}, {actual_delivery: {_gt: $start}}, {actual_delivery: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}, {_or: [{ownr_ph1: {_is_null: false}}, {ownr_ea: {_is_null: false}}]}]}) {
|
jobs(where: {_and: [{converted: {_eq: true}}, {actual_delivery: {_gt: $start}}, {actual_delivery: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}, {_or: [{ownr_ph1: {_is_null: false}}, {ownr_ea: {_is_null: false}}]}]}) {
|
||||||
|
actual_delivery
|
||||||
id
|
id
|
||||||
created_at
|
created_at
|
||||||
ro_number
|
ro_number
|
||||||
@@ -1365,6 +1388,7 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) {
|
|||||||
cieca_pfl
|
cieca_pfl
|
||||||
cieca_pft
|
cieca_pft
|
||||||
cieca_pfo
|
cieca_pfo
|
||||||
|
cieca_ttl
|
||||||
vehicle {
|
vehicle {
|
||||||
id
|
id
|
||||||
notes
|
notes
|
||||||
@@ -1494,7 +1518,8 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) {
|
|||||||
}`;
|
}`;
|
||||||
//TODO:AIO The above query used to have parts order lines in it. Validate that this doesn't need it.
|
//TODO:AIO The above query used to have parts order lines in it. Validate that this doesn't need it.
|
||||||
|
|
||||||
exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
|
exports.QUERY_JOB_COSTING_DETAILS = `
|
||||||
|
query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
|
||||||
jobs_by_pk(id: $id) {
|
jobs_by_pk(id: $id) {
|
||||||
ro_number
|
ro_number
|
||||||
clm_total
|
clm_total
|
||||||
@@ -2544,3 +2569,44 @@ exports.GET_JOBS_BY_PKS = `query GET_JOBS_BY_PKS($ids: [uuid!]!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports.MARK_MESSAGES_AS_READ = `mutation MARK_MESSAGES_AS_READ($conversationId: uuid!) {
|
||||||
|
update_messages(where: { conversationid: { _eq: $conversationId } }, _set: { read: true }) {
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
affected_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports.CREATE_CONVERSATION = `mutation CREATE_CONVERSATION($conversation: [conversations_insert_input!]!) {
|
||||||
|
insert_conversations(objects: $conversation) {
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
phone_num
|
||||||
|
archived
|
||||||
|
label
|
||||||
|
unreadcnt
|
||||||
|
job_conversations {
|
||||||
|
jobid
|
||||||
|
conversationid
|
||||||
|
job {
|
||||||
|
id
|
||||||
|
ro_number
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false } }) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -149,6 +149,58 @@ exports.generate_payment_url = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Reference: https://intellipay.com/dist/webapi26.html#operation/fee
|
||||||
|
exports.checkfee = async (req, res) => {
|
||||||
|
// Requires amount, bodyshop.imexshopid, and state? to get data.
|
||||||
|
logger.log("intellipay-fee-check", "DEBUG", req.user?.email, null, null);
|
||||||
|
|
||||||
|
//If there's no amount, there can't be a fee. Skip the call.
|
||||||
|
if (!req.body.amount || req.body.amount <= 0) {
|
||||||
|
res.json({ fee: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||||
|
//TODO: Move these to environment variables/database.
|
||||||
|
data: qs.stringify(
|
||||||
|
{
|
||||||
|
method: "fee",
|
||||||
|
...shopCredentials,
|
||||||
|
amount: req.body.amount,
|
||||||
|
paymenttype: `CC`,
|
||||||
|
cardnum: "4111111111111111", //Not needed per documentation, but incorrect values come back without it.
|
||||||
|
state:
|
||||||
|
req.body.bodyshop?.state && req.body.bodyshop.state?.length === 2
|
||||||
|
? req.body.bodyshop.state.toUpperCase()
|
||||||
|
: "ZZ" //Same as above
|
||||||
|
},
|
||||||
|
{ sort: false } //ColdFusion Query Strings depend on order. This preserves it.
|
||||||
|
),
|
||||||
|
url: `https://${domain}.cpteller.com/api/26/webapi.cfc`
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios(options);
|
||||||
|
if (response.data?.error) {
|
||||||
|
res.status(400).json({ error: response.data.error });
|
||||||
|
} else if (response.data < 0) {
|
||||||
|
res.json({ error: "Fee amount negative. Check API credentials & account configuration." });
|
||||||
|
} else {
|
||||||
|
res.json({ fee: response.data });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
//console.log(error);
|
||||||
|
logger.log("intellipay-fee-check-error", "ERROR", req.user?.email, null, {
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
res.status(400).json({ error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
exports.postback = async (req, res) => {
|
exports.postback = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
logger.log("intellipay-postback", "DEBUG", req.user?.email, null, req.body);
|
logger.log("intellipay-postback", "DEBUG", req.user?.email, null, req.body);
|
||||||
|
|||||||
@@ -848,6 +848,41 @@ function GenerateCostingData(job) {
|
|||||||
gppercent: formatGpPercent(0)
|
gppercent: formatGpPercent(0)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
//Push adjustments to bottom line.
|
||||||
|
if (job.job_totals?.totals?.ttl_adjustment) {
|
||||||
|
//Add to totals.
|
||||||
|
const Adjustment = Dinero(job.job_totals.totals.ttl_adjustment); //Need to invert, since this is being assigned as a cost.
|
||||||
|
summaryData.totalAdditionalSales = summaryData.totalAdditionalSales.add(Adjustment);
|
||||||
|
summaryData.totalSales = summaryData.totalSales.add(Adjustment);
|
||||||
|
//Add to lines.
|
||||||
|
costCenterData.push({
|
||||||
|
id: "AdjEst",
|
||||||
|
cost_center: "Adjustment (Est. Match)",
|
||||||
|
sale_labor: Dinero().toFormat(),
|
||||||
|
sale_labor_dinero: Dinero(),
|
||||||
|
sale_parts: Dinero().toFormat(),
|
||||||
|
sale_parts_dinero: Dinero(),
|
||||||
|
sale_additional: Adjustment.toFormat(),
|
||||||
|
sale_additional_dinero: Adjustment,
|
||||||
|
sale_sublet: Dinero(),
|
||||||
|
sale_sublet_dinero: Dinero(),
|
||||||
|
sales: Adjustment.toFormat(),
|
||||||
|
sales_dinero: Adjustment,
|
||||||
|
cost_parts: Dinero().toFormat(),
|
||||||
|
cost_parts_dinero: Dinero(),
|
||||||
|
cost_labor: Dinero().toFormat(), //Adjustment.toFormat(),
|
||||||
|
cost_labor_dinero: Dinero(), // Adjustment,
|
||||||
|
cost_additional: Dinero(),
|
||||||
|
cost_additional_dinero: Dinero(),
|
||||||
|
cost_sublet: Dinero(),
|
||||||
|
cost_sublet_dinero: Dinero(),
|
||||||
|
costs: Dinero().toFormat(),
|
||||||
|
costs_dinero: Dinero(),
|
||||||
|
gpdollars_dinero: Dinero(),
|
||||||
|
gpdollars: Dinero().toFormat(),
|
||||||
|
gppercent: formatGpPercent(0)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//Final summary data massaging.
|
//Final summary data massaging.
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ exports.totalsSsu = async function (req, res) {
|
|||||||
} catch (error) {
|
} 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,
|
jobid: id,
|
||||||
error
|
error: error.message
|
||||||
});
|
});
|
||||||
res.status(503).send();
|
res.status(503).send();
|
||||||
}
|
}
|
||||||
@@ -68,6 +68,54 @@ async function TotalsServerSide(req, res) {
|
|||||||
ret.additional = CalculateAdditional(job);
|
ret.additional = CalculateAdditional(job);
|
||||||
ret.totals = CalculateTaxesTotals(job, ret);
|
ret.totals = CalculateTaxesTotals(job, ret);
|
||||||
|
|
||||||
|
// Sub total scrubbbing.
|
||||||
|
const emsTotal =
|
||||||
|
job.cieca_ttl.data.n_ttl_amt === job.cieca_ttl.data.g_ttl_amt //It looks like sometimes, gross and net are the same, but they shouldn't be.
|
||||||
|
? job.cieca_ttl.data.n_ttl_amt - job.cieca_ttl.data.g_tax
|
||||||
|
: job.cieca_ttl.data.g_ttl_amt - job.cieca_ttl.data.g_tax; //If they are, adjust the gross total down by the tax amount.
|
||||||
|
const ttlDifference =
|
||||||
|
emsTotal -
|
||||||
|
ret.totals.subtotal
|
||||||
|
.add(
|
||||||
|
Dinero({
|
||||||
|
amount: Math.round((job.adjustment_bottom_line || 0) * 100)
|
||||||
|
}).multiply(-1) //Add back in the adjustment to the subtotal. We don't want to scrub it twice.
|
||||||
|
)
|
||||||
|
.getAmount() /
|
||||||
|
100;
|
||||||
|
|
||||||
|
if (Math.abs(ttlDifference) > 0.0) {
|
||||||
|
//If difference is greater than a pennny, we need to adjust it.
|
||||||
|
ret.totals.ttl_adjustment = Dinero({ amount: Math.round(ttlDifference * 100) });
|
||||||
|
ret.totals.subtotal = ret.totals.subtotal.add(ret.totals.ttl_adjustment);
|
||||||
|
ret.totals.total_repairs = ret.totals.total_repairs.add(ret.totals.ttl_adjustment);
|
||||||
|
ret.totals.net_repairs = ret.totals.net_repairs.add(ret.totals.ttl_adjustment);
|
||||||
|
logger.log("job-totals-USA-ttl-adj", "DEBUG", null, job.id, {
|
||||||
|
adjAmount: ttlDifference
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Taxes Scrubbing
|
||||||
|
const emsTaxTotal = job.cieca_ttl.data.g_tax;
|
||||||
|
const totalUsTaxes =
|
||||||
|
(ret.totals.us_sales_tax_breakdown.ty1Tax.getAmount() +
|
||||||
|
ret.totals.us_sales_tax_breakdown.ty2Tax.getAmount() +
|
||||||
|
ret.totals.us_sales_tax_breakdown.ty3Tax.getAmount() +
|
||||||
|
ret.totals.us_sales_tax_breakdown.ty4Tax.getAmount() +
|
||||||
|
ret.totals.us_sales_tax_breakdown.ty5Tax.getAmount()) /
|
||||||
|
100;
|
||||||
|
const ttlTaxDifference = emsTaxTotal - totalUsTaxes;
|
||||||
|
|
||||||
|
if (Math.abs(ttlTaxDifference) > 0.0) {
|
||||||
|
//If difference is greater than a pennny, we need to adjust it.
|
||||||
|
ret.totals.ttl_tax_adjustment = Dinero({ amount: Math.round(ttlTaxDifference * 100) });
|
||||||
|
ret.totals.total_repairs = ret.totals.total_repairs.add(ret.totals.ttl_tax_adjustment);
|
||||||
|
ret.totals.net_repairs = ret.totals.net_repairs.add(ret.totals.ttl_tax_adjustment);
|
||||||
|
logger.log("job-totals-USA-ttl-tax-adj", "DEBUG", null, job.id, {
|
||||||
|
adjAmount: ttlTaxDifference
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-ssu-USA-error", "ERROR", req.user?.email, job.id, {
|
logger.log("job-totals-ssu-USA-error", "ERROR", req.user?.email, job.id, {
|
||||||
@@ -842,17 +890,21 @@ function CalculateTaxesTotals(job, otherTotals) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
//Add towing and storage taxable amounts
|
//Add towing and storage taxable amounts
|
||||||
const stlTowing = job.cieca_stl?.data.find((c) => c.ttl_typecd === "OTTW");
|
const stlTowing = job.cieca_stl?.data.find((c) => c.ttl_typecd === "OTTW" || c.ttl_type === "OTTW");
|
||||||
const stlStorage = job.cieca_stl?.data.find((c) => c.ttl_typecd === "OTST");
|
const stlStorage = job.cieca_stl?.data.find((c) => c.ttl_typecd === "OTST" || c.ttl_type === "OTST");
|
||||||
|
|
||||||
if (stlTowing)
|
if (stlTowing)
|
||||||
taxableAmounts.TOW = Dinero({
|
taxableAmounts.TOW = taxableAmounts.TOW.add(
|
||||||
amount: Math.round(stlTowing.t_amt * 100)
|
Dinero({
|
||||||
});
|
amount: Math.round(stlTowing.t_amt * 100)
|
||||||
|
})
|
||||||
|
);
|
||||||
if (stlStorage)
|
if (stlStorage)
|
||||||
taxableAmounts.TOW = Dinero({
|
taxableAmounts.TOW = taxableAmounts.TOW.add(
|
||||||
amount: Math.round(stlStorage.t_amt * 100)
|
(taxableAmounts.TOW = Dinero({
|
||||||
});
|
amount: Math.round(stlStorage.t_amt * 100)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const pfp = job.parts_tax_rates;
|
const pfp = job.parts_tax_rates;
|
||||||
|
|
||||||
@@ -959,7 +1011,7 @@ function CalculateTaxesTotals(job, otherTotals) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-USA Key with issue", "error", null, null, {
|
logger.log("job-totals-USA Key with issue", "error", null, job.id, {
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -989,7 +1041,7 @@ function CalculateTaxesTotals(job, otherTotals) {
|
|||||||
for (let threshCounter = 1; threshCounter <= 5; threshCounter++) {
|
for (let threshCounter = 1; threshCounter <= 5; threshCounter++) {
|
||||||
const thresholdAmount = parseFloat(job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`]) || 0;
|
const thresholdAmount = parseFloat(job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`]) || 0;
|
||||||
const thresholdTaxRate = parseFloat(job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`]) || 0;
|
const thresholdTaxRate = parseFloat(job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`]) || 0;
|
||||||
|
// console.log(taxTierKey, tyCounter, threshCounter, thresholdAmount, thresholdTaxRate);
|
||||||
let taxableAmountInThisThreshold;
|
let taxableAmountInThisThreshold;
|
||||||
if (
|
if (
|
||||||
thresholdAmount === 9999.99 ||
|
thresholdAmount === 9999.99 ||
|
||||||
@@ -1012,11 +1064,8 @@ function CalculateTaxesTotals(job, otherTotals) {
|
|||||||
taxableAmountInThisThreshold = Dinero({
|
taxableAmountInThisThreshold = Dinero({
|
||||||
amount: Math.round(thresholdAmount * 100)
|
amount: Math.round(thresholdAmount * 100)
|
||||||
});
|
});
|
||||||
remainingTaxableAmounts[taxTierKey] = remainingTaxableAmounts[taxTierKey].subtract(
|
remainingTaxableAmounts[taxTierKey] =
|
||||||
Dinero({
|
remainingTaxableAmounts[taxTierKey].subtract(taxableAmountInThisThreshold);
|
||||||
amount: Math.round(taxableAmountInThisThreshold * 100)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,8 +1074,8 @@ function CalculateTaxesTotals(job, otherTotals) {
|
|||||||
totalTaxByTier[taxTierKey] = totalTaxByTier[taxTierKey].add(taxAmountToAdd);
|
totalTaxByTier[taxTierKey] = totalTaxByTier[taxTierKey].add(taxAmountToAdd);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("job-totals-USA - PFP Calculation Error", "error", null, null, {
|
logger.log("job-totals-USA - PFP Calculation Error", "error", null, job.id, {
|
||||||
error
|
error: error.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
const { lightbox_credentials, payment_refund, generate_payment_url, postback } = require("../intellipay/intellipay");
|
const { lightbox_credentials, payment_refund, generate_payment_url, postback, checkfee } = require("../intellipay/intellipay");
|
||||||
|
|
||||||
router.post("/lightbox_credentials", validateFirebaseIdTokenMiddleware, lightbox_credentials);
|
router.post("/lightbox_credentials", validateFirebaseIdTokenMiddleware, lightbox_credentials);
|
||||||
router.post("/payment_refund", validateFirebaseIdTokenMiddleware, payment_refund);
|
router.post("/payment_refund", validateFirebaseIdTokenMiddleware, payment_refund);
|
||||||
router.post("/generate_payment_url", validateFirebaseIdTokenMiddleware, generate_payment_url);
|
router.post("/generate_payment_url", validateFirebaseIdTokenMiddleware, generate_payment_url);
|
||||||
|
router.post("/checkfee", validateFirebaseIdTokenMiddleware, checkfee);
|
||||||
router.post("/postback", postback);
|
router.post("/postback", postback);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLCl
|
|||||||
const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
||||||
const { canvastest } = require("../render/canvas-handler");
|
const { canvastest } = require("../render/canvas-handler");
|
||||||
const { alertCheck } = require("../alerts/alertcheck");
|
const { alertCheck } = require("../alerts/alertcheck");
|
||||||
|
const uuid = require("uuid").v4;
|
||||||
|
|
||||||
//Test route to ensure Express is responding.
|
//Test route to ensure Express is responding.
|
||||||
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
|
router.get("/test", eventAuthorizationMiddleware, async function (req, res) {
|
||||||
@@ -57,6 +58,59 @@ router.get("/test-logs", eventAuthorizationMiddleware, (req, res) => {
|
|||||||
|
|
||||||
return res.status(500).send("Logs tested.");
|
return res.status(500).send("Logs tested.");
|
||||||
});
|
});
|
||||||
|
router.get("/wstest", eventAuthorizationMiddleware, (req, res) => {
|
||||||
|
const { ioRedis } = req;
|
||||||
|
ioRedis.to(`bodyshop-broadcast-room:bfec8c8c-b7f1-49e0-be4c-524455f4e582`).emit("new-message-summary", {
|
||||||
|
isoutbound: true,
|
||||||
|
conversationId: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6",
|
||||||
|
msid: "SM5d053957bc0da29399b768c23bffcc0f",
|
||||||
|
summary: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Do we need to add more content here?
|
||||||
|
ioRedis
|
||||||
|
.to(`bodyshop-conversation-room:bfec8c8c-b7f1-49e0-be4c-524455f4e582:2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6`)
|
||||||
|
.emit("new-message-detailed", {
|
||||||
|
//
|
||||||
|
// msid: "SMbbd7703a898fef7f2c07c148ade8a6cd",
|
||||||
|
// text: "test2",
|
||||||
|
// conversationid: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6",
|
||||||
|
// isoutbound: true,
|
||||||
|
// userid: "patrick@imex.dev",
|
||||||
|
// image: false,
|
||||||
|
// image_path: [],
|
||||||
|
newMessage: {
|
||||||
|
conversation: {
|
||||||
|
id: uuid(),
|
||||||
|
archived: false,
|
||||||
|
bodyshop: {
|
||||||
|
id: "bfec8c8c-b7f1-49e0-be4c-524455f4e582",
|
||||||
|
imexshopid: "APPLE"
|
||||||
|
},
|
||||||
|
created_at: "2024-11-19T19:46:38.984633+00:00",
|
||||||
|
updated_at: "2024-11-19T22:40:48.346875+00:00",
|
||||||
|
unreadcnt: 0,
|
||||||
|
phone_num: "+16138676684"
|
||||||
|
},
|
||||||
|
conversationid: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6",
|
||||||
|
created_at: "2024-11-19T22:40:48.346875+00:00",
|
||||||
|
id: "68604ea9-c411-43ec-ab83-899868e58819",
|
||||||
|
image_path: [],
|
||||||
|
image: false,
|
||||||
|
isoutbound: true,
|
||||||
|
msid: "SMbbd7703a898fef7f2c07c148ade8a6cd",
|
||||||
|
read: false,
|
||||||
|
text: `This is a test ${Math.round(Math.random() * 100)}`,
|
||||||
|
updated_at: "2024-11-19T22:40:48.346875+00:00",
|
||||||
|
status: "posted",
|
||||||
|
userid: "patrick@imex.dev"
|
||||||
|
},
|
||||||
|
conversationId: "2b44d692-a9e4-4ed4-9c6b-7d8b0c44a0f6",
|
||||||
|
summary: false
|
||||||
|
}); // TODO: Do we need to add more content here?
|
||||||
|
|
||||||
|
return res.status(500).send("Logs tested.");
|
||||||
|
});
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search);
|
router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
const { subscribe, unsubscribe } = require("../firebase/firebase-handler");
|
const { subscribe, unsubscribe, sendNotification } = require("../firebase/firebase-handler");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use(validateFirebaseIdTokenMiddleware);
|
router.use(validateFirebaseIdTokenMiddleware);
|
||||||
|
|
||||||
router.post("/subscribe", subscribe);
|
router.post("/subscribe", subscribe);
|
||||||
router.post("/unsubscribe", unsubscribe);
|
router.post("/unsubscribe", unsubscribe);
|
||||||
|
router.post("/sendtestnotification", sendNotification);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -11,186 +11,161 @@ const logger = require("../utils/logger");
|
|||||||
const InstanceManager = require("../utils/instanceMgr").default;
|
const InstanceManager = require("../utils/instanceMgr").default;
|
||||||
|
|
||||||
exports.receive = async (req, res) => {
|
exports.receive = async (req, res) => {
|
||||||
//Perform request validation
|
const {
|
||||||
|
ioRedis,
|
||||||
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
|
} = req;
|
||||||
|
|
||||||
logger.log("sms-inbound", "DEBUG", "api", null, {
|
const loggerData = {
|
||||||
msid: req.body.SmsMessageSid,
|
msid: req.body.SmsMessageSid,
|
||||||
text: req.body.Body,
|
text: req.body.Body,
|
||||||
image: !!req.body.MediaUrl0,
|
image: !!req.body.MediaUrl0,
|
||||||
image_path: generateMediaArray(req.body)
|
image_path: generateMediaArray(req.body)
|
||||||
});
|
};
|
||||||
|
|
||||||
if (!!!req.body || !!!req.body.MessagingServiceSid || !!!req.body.SmsMessageSid) {
|
logger.log("sms-inbound", "DEBUG", "api", null, loggerData);
|
||||||
|
|
||||||
|
if (!req.body || !req.body.MessagingServiceSid || !req.body.SmsMessageSid) {
|
||||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||||
|
...loggerData,
|
||||||
|
type: "malformed-request"
|
||||||
|
});
|
||||||
|
return res.status(400).json({ success: false, error: "Malformed Request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Find the bodyshop and existing conversation
|
||||||
|
const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
|
||||||
|
mssid: req.body.MessagingServiceSid,
|
||||||
|
phone: phone(req.body.From).phoneNumber
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.bodyshops[0]) {
|
||||||
|
return res.status(400).json({ success: false, error: "No matching bodyshop" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyshop = response.bodyshops[0];
|
||||||
|
|
||||||
|
// Sort conversations by `updated_at` (or `created_at`) and pick the last one
|
||||||
|
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||||
|
const existingConversation = sortedConversations.length
|
||||||
|
? sortedConversations[sortedConversations.length - 1]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let conversationid;
|
||||||
|
let newMessage = {
|
||||||
msid: req.body.SmsMessageSid,
|
msid: req.body.SmsMessageSid,
|
||||||
text: req.body.Body,
|
text: req.body.Body,
|
||||||
image: !!req.body.MediaUrl0,
|
image: !!req.body.MediaUrl0,
|
||||||
image_path: generateMediaArray(req.body),
|
image_path: generateMediaArray(req.body),
|
||||||
type: "malformed-request"
|
isoutbound: false,
|
||||||
});
|
userid: null // Add additional fields as necessary
|
||||||
res.status(400);
|
};
|
||||||
res.json({ success: false, error: "Malformed Request" });
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
|
|
||||||
mssid: req.body.MessagingServiceSid,
|
|
||||||
phone: phone(req.body.From).phoneNumber
|
|
||||||
});
|
|
||||||
|
|
||||||
let newMessage = {
|
if (existingConversation) {
|
||||||
msid: req.body.SmsMessageSid,
|
// Use the existing conversation
|
||||||
text: req.body.Body,
|
conversationid = existingConversation.id;
|
||||||
image: !!req.body.MediaUrl0,
|
|
||||||
image_path: generateMediaArray(req.body)
|
|
||||||
};
|
|
||||||
if (response.bodyshops[0]) {
|
|
||||||
//Found a bodyshop - should always happen.
|
|
||||||
if (response.bodyshops[0].conversations.length === 0) {
|
|
||||||
//No conversation Found, create one.
|
|
||||||
//console.log("[SMS Receive] No conversation found. Creating one.");
|
|
||||||
newMessage.conversation = {
|
|
||||||
data: {
|
|
||||||
bodyshopid: response.bodyshops[0].id,
|
|
||||||
phone_num: phone(req.body.From).phoneNumber
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else if (response.bodyshops[0].conversations.length === 1) {
|
|
||||||
//Just add it to the conversation
|
|
||||||
//console.log("[SMS Receive] Conversation found. Added ID.");
|
|
||||||
newMessage.conversationid = response.bodyshops[0].conversations[0].id;
|
|
||||||
} else {
|
|
||||||
//We should never get here.
|
|
||||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
|
||||||
msid: req.body.SmsMessageSid,
|
|
||||||
text: req.body.Body,
|
|
||||||
image: !!req.body.MediaUrl0,
|
|
||||||
image_path: generateMediaArray(req.body),
|
|
||||||
messagingServiceSid: req.body.MessagingServiceSid,
|
|
||||||
type: "duplicate-phone"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
let insertresp;
|
|
||||||
if (response.bodyshops[0].conversations[0]) {
|
|
||||||
insertresp = await client.request(queries.INSERT_MESSAGE, {
|
|
||||||
msg: newMessage,
|
|
||||||
conversationid: response.bodyshops[0].conversations[0] && response.bodyshops[0].conversations[0].id
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
insertresp = await client.request(queries.RECEIVE_MESSAGE, {
|
|
||||||
msg: newMessage
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const message = insertresp.insert_messages.returning[0];
|
|
||||||
const data = {
|
|
||||||
type: "messaging-inbound",
|
|
||||||
conversationid: message.conversationid || "",
|
|
||||||
text: message.text || "",
|
|
||||||
messageid: message.id || "",
|
|
||||||
phone_num: message.conversation.phone_num || ""
|
|
||||||
};
|
|
||||||
|
|
||||||
const fcmresp = await admin.messaging().send({
|
// Unarchive the conversation if necessary
|
||||||
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
if (existingConversation.archived) {
|
||||||
notification: {
|
await client.request(queries.UNARCHIVE_CONVERSATION, {
|
||||||
title: InstanceManager({
|
id: conversationid,
|
||||||
imex: `ImEX Online Message - ${data.phone_num}`,
|
archived: false
|
||||||
rome: `Rome Online Message - ${data.phone_num}`
|
});
|
||||||
}),
|
|
||||||
body: message.image_path ? `Image ${message.text}` : message.text
|
|
||||||
//imageUrl: "https://thinkimex.com/img/io-fcm.png", //TODO:AIO Resolve addresses for other instances
|
|
||||||
},
|
|
||||||
data
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log("sms-inbound-success", "DEBUG", "api", null, {
|
|
||||||
newMessage,
|
|
||||||
fcmresp
|
|
||||||
});
|
|
||||||
res.status(200).send("");
|
|
||||||
} catch (e2) {
|
|
||||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
|
||||||
msid: req.body.SmsMessageSid,
|
|
||||||
text: req.body.Body,
|
|
||||||
image: !!req.body.MediaUrl0,
|
|
||||||
image_path: generateMediaArray(req.body),
|
|
||||||
messagingServiceSid: req.body.MessagingServiceSid,
|
|
||||||
error: e2
|
|
||||||
});
|
|
||||||
|
|
||||||
res.sendStatus(500).json(e2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e1) {
|
} else {
|
||||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
// Create a new conversation
|
||||||
msid: req.body.SmsMessageSid,
|
const newConversationResponse = await client.request(queries.CREATE_CONVERSATION, {
|
||||||
text: req.body.Body,
|
conversation: {
|
||||||
image: !!req.body.MediaUrl0,
|
bodyshopid: bodyshop.id,
|
||||||
image_path: generateMediaArray(req.body),
|
phone_num: phone(req.body.From).phoneNumber,
|
||||||
messagingServiceSid: req.body.MessagingServiceSid,
|
archived: false
|
||||||
error: e1
|
}
|
||||||
});
|
});
|
||||||
res.sendStatus(500).json(e1);
|
const createdConversation = newConversationResponse.insert_conversations.returning[0];
|
||||||
|
conversationid = createdConversation.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure `conversationid` is added to the message
|
||||||
|
newMessage.conversationid = conversationid;
|
||||||
|
|
||||||
|
// Step 3: Insert the message into the conversation
|
||||||
|
const insertresp = await client.request(queries.INSERT_MESSAGE, {
|
||||||
|
msg: newMessage,
|
||||||
|
conversationid: conversationid
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = insertresp?.insert_messages?.returning?.[0];
|
||||||
|
const conversation = message?.conversation || null;
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error("Conversation data is missing from the response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Notify clients through Redis
|
||||||
|
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
|
||||||
|
const conversationRoom = getBodyshopConversationRoom({
|
||||||
|
bodyshopId: conversation.bodyshop.id,
|
||||||
|
conversationId: conversation.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const commonPayload = {
|
||||||
|
isoutbound: false,
|
||||||
|
conversationId: conversation.id,
|
||||||
|
updated_at: message.updated_at,
|
||||||
|
msid: message.sid
|
||||||
|
};
|
||||||
|
|
||||||
|
ioRedis.to(broadcastRoom).emit("new-message-summary", {
|
||||||
|
...commonPayload,
|
||||||
|
existingConversation: !!existingConversation,
|
||||||
|
newConversation: !existingConversation ? conversation : null,
|
||||||
|
summary: true
|
||||||
|
});
|
||||||
|
|
||||||
|
ioRedis.to(conversationRoom).emit("new-message-detailed", {
|
||||||
|
newMessage: message,
|
||||||
|
...commonPayload,
|
||||||
|
newConversation: !existingConversation ? conversation : null,
|
||||||
|
existingConversation: !!existingConversation,
|
||||||
|
summary: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 5: Send FCM notification
|
||||||
|
const fcmresp = await admin.messaging().send({
|
||||||
|
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
||||||
|
notification: {
|
||||||
|
title: InstanceManager({
|
||||||
|
imex: `ImEX Online Message - ${message.conversation.phone_num}`,
|
||||||
|
rome: `Rome Online Message - ${message.conversation.phone_num}`,
|
||||||
|
}),
|
||||||
|
body: message.image_path ? `Image ${message.text}` : message.text
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: "messaging-inbound",
|
||||||
|
conversationid: message.conversationid || "",
|
||||||
|
text: message.text || "",
|
||||||
|
messageid: message.id || "",
|
||||||
|
phone_num: message.conversation.phone_num || ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log("sms-inbound-success", "DEBUG", "api", null, {
|
||||||
|
newMessage,
|
||||||
|
fcmresp
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send("");
|
||||||
|
} catch (e) {
|
||||||
|
handleError(req, e, res, "RECEIVE_MESSAGE");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// const sampleMessage: {
|
|
||||||
// "ToCountry": "CA",
|
|
||||||
// "ToState": "BC",
|
|
||||||
// "SmsMessageSid": "SMad7bddaf3454c0904999d6018b1e8f49",
|
|
||||||
// "NumMedia": "0",
|
|
||||||
// "ToCity": "Vancouver",
|
|
||||||
// "FromZip": "",
|
|
||||||
// "SmsSid": "SMad7bddaf3454c0904999d6018b1e8f49",
|
|
||||||
// "FromState": "BC",
|
|
||||||
// "SmsStatus": "received",
|
|
||||||
// "FromCity": "VANCOUVER",
|
|
||||||
// "Body": "Hi",
|
|
||||||
// "FromCountry": "CA",
|
|
||||||
// "To": "+16043301606",
|
|
||||||
// "MessagingServiceSid": "MG6e259e2add04ffa0d0aa355038670ee1",
|
|
||||||
// "ToZip": "",
|
|
||||||
// "NumSegments": "1",
|
|
||||||
// "MessageSid": "SMad7bddaf3454c0904999d6018b1e8f49",
|
|
||||||
// "AccountSid": "AC6c09d337d6b9c68ab6488c2052bd457c",
|
|
||||||
// "From": "+16049992002",
|
|
||||||
// "ApiVersion": "2010-04-01"
|
|
||||||
// }
|
|
||||||
// ] req.body {
|
|
||||||
// [0] ToCountry: 'CA',
|
|
||||||
// [0] MediaContentType0: 'image/jpeg',
|
|
||||||
// [0] ToState: 'BC',
|
|
||||||
// [0] SmsMessageSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27',
|
|
||||||
// [0] NumMedia: '1',
|
|
||||||
// [0] ToCity: 'Vancouver',
|
|
||||||
// [0] FromZip: '',
|
|
||||||
// [0] SmsSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27',
|
|
||||||
// [0] FromState: 'BC',
|
|
||||||
// [0] SmsStatus: 'received',
|
|
||||||
// [0] FromCity: 'VANCOUVER',
|
|
||||||
// [0] Body: '',
|
|
||||||
// [0] FromCountry: 'CA',
|
|
||||||
// [0] To: '+16043301606',
|
|
||||||
// [0] MessagingServiceSid: 'MG6e259e2add04ffa0d0aa355038670ee1',
|
|
||||||
// [0] ToZip: '',
|
|
||||||
// [0] NumSegments: '1',
|
|
||||||
// [0] MessageSid: 'MM14fa2851ba26e0dc2b62073f8e7cdf27',
|
|
||||||
// [0] AccountSid: 'AC6c09d337d6b9c68ab6488c2052bd457c',
|
|
||||||
// [0] From: '+16049992002',
|
|
||||||
// [0] MediaUrl0: 'https://api.twilio.com/2010-04-01/Accounts/AC6c09d337d6b9c68ab6488c2052bd457c/Messages/MM14fa2851ba26e0dc2b62073f8e7cdf27/Media/MEf129dd37979852f395eb29ffb126e19e',
|
|
||||||
// [0] ApiVersion: '2010-04-01'
|
|
||||||
// [0] }
|
|
||||||
|
|
||||||
// [0] MediaContentType0: 'image/jpeg',
|
|
||||||
// MediaContentType0: 'video/3gpp',
|
|
||||||
|
|
||||||
const generateMediaArray = (body) => {
|
const generateMediaArray = (body) => {
|
||||||
const { NumMedia } = body;
|
const { NumMedia } = body;
|
||||||
if (parseInt(NumMedia) > 0) {
|
if (parseInt(NumMedia) > 0) {
|
||||||
//stuff
|
|
||||||
const ret = [];
|
const ret = [];
|
||||||
for (var i = 0; i < parseInt(NumMedia); i++) {
|
for (let i = 0; i < parseInt(NumMedia); i++) {
|
||||||
ret.push(body[`MediaUrl${i}`]);
|
ret.push(body[`MediaUrl${i}`]);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
@@ -198,3 +173,17 @@ const generateMediaArray = (body) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleError = (req, error, res, context) => {
|
||||||
|
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||||
|
msid: req.body.SmsMessageSid,
|
||||||
|
text: req.body.Body,
|
||||||
|
image: !!req.body.MediaUrl0,
|
||||||
|
image_path: generateMediaArray(req.body),
|
||||||
|
messagingServiceSid: req.body.MessagingServiceSid,
|
||||||
|
context,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(500).json({ error: error.message || "Internal Server Error" });
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ const { phone } = require("phone");
|
|||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
|
const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
|
||||||
const { admin } = require("../firebase/firebase-handler");
|
|
||||||
|
|
||||||
const gqlClient = require("../graphql-client/graphql-client").client;
|
const gqlClient = require("../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
exports.send = (req, res) => {
|
exports.send = async (req, res) => {
|
||||||
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body;
|
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body;
|
||||||
|
const {
|
||||||
|
ioRedis,
|
||||||
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
|
} = req;
|
||||||
|
|
||||||
logger.log("sms-outbound", "DEBUG", req.user.email, null, {
|
logger.log("sms-outbound", "DEBUG", req.user.email, null, {
|
||||||
messagingServiceSid: messagingServiceSid,
|
messagingServiceSid,
|
||||||
to: phone(to).phoneNumber,
|
to: phone(to).phoneNumber,
|
||||||
mediaUrl: selectedMedia.map((i) => i.src),
|
mediaUrl: selectedMedia.map((i) => i.src),
|
||||||
text: body,
|
text: body,
|
||||||
@@ -27,66 +29,10 @@ exports.send = (req, res) => {
|
|||||||
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!!to && !!messagingServiceSid && (!!body || !!selectedMedia.length > 0) && !!conversationid) {
|
if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid) {
|
||||||
client.messages
|
|
||||||
.create({
|
|
||||||
body: body,
|
|
||||||
messagingServiceSid: messagingServiceSid,
|
|
||||||
to: phone(to).phoneNumber,
|
|
||||||
mediaUrl: selectedMedia.map((i) => i.src)
|
|
||||||
})
|
|
||||||
.then((message) => {
|
|
||||||
let newMessage = {
|
|
||||||
msid: message.sid,
|
|
||||||
text: body,
|
|
||||||
conversationid,
|
|
||||||
isoutbound: true,
|
|
||||||
userid: req.user.email,
|
|
||||||
image: req.body.selectedMedia.length > 0,
|
|
||||||
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
|
||||||
};
|
|
||||||
gqlClient
|
|
||||||
.request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid })
|
|
||||||
.then((r2) => {
|
|
||||||
//console.log("Responding GQL Message ID", JSON.stringify(r2));
|
|
||||||
logger.log("sms-outbound-success", "DEBUG", req.user.email, null, {
|
|
||||||
msid: message.sid,
|
|
||||||
conversationid
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
type: "messaging-outbound",
|
|
||||||
conversationid: newMessage.conversationid || ""
|
|
||||||
};
|
|
||||||
|
|
||||||
admin.messaging().send({
|
|
||||||
topic: `${imexshopid}-messaging`,
|
|
||||||
data
|
|
||||||
});
|
|
||||||
|
|
||||||
res.sendStatus(200);
|
|
||||||
})
|
|
||||||
.catch((e2) => {
|
|
||||||
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
|
||||||
msid: message.sid,
|
|
||||||
conversationid,
|
|
||||||
error: e2
|
|
||||||
});
|
|
||||||
|
|
||||||
//res.json({ success: false, message: e2 });
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((e1) => {
|
|
||||||
//res.json({ success: false, message: error });
|
|
||||||
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
|
||||||
conversationid,
|
|
||||||
error: e1
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
||||||
type: "missing-parameters",
|
type: "missing-parameters",
|
||||||
messagingServiceSid: messagingServiceSid,
|
messagingServiceSid,
|
||||||
to: phone(to).phoneNumber,
|
to: phone(to).phoneNumber,
|
||||||
text: body,
|
text: body,
|
||||||
conversationid,
|
conversationid,
|
||||||
@@ -96,5 +42,72 @@ exports.send = (req, res) => {
|
|||||||
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||||
});
|
});
|
||||||
res.status(400).json({ success: false, message: "Missing required parameter(s)." });
|
res.status(400).json({ success: false, message: "Missing required parameter(s)." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await client.messages.create({
|
||||||
|
body,
|
||||||
|
messagingServiceSid,
|
||||||
|
to: phone(to).phoneNumber,
|
||||||
|
mediaUrl: selectedMedia.map((i) => i.src)
|
||||||
|
});
|
||||||
|
|
||||||
|
const newMessage = {
|
||||||
|
msid: message.sid,
|
||||||
|
text: body,
|
||||||
|
conversationid,
|
||||||
|
isoutbound: true,
|
||||||
|
userid: req.user.email,
|
||||||
|
image: req.body.selectedMedia.length > 0,
|
||||||
|
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gqlResponse = await gqlClient.request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid });
|
||||||
|
|
||||||
|
logger.log("sms-outbound-success", "DEBUG", req.user.email, null, {
|
||||||
|
msid: message.sid,
|
||||||
|
conversationid
|
||||||
|
});
|
||||||
|
|
||||||
|
const insertedMessage = gqlResponse.insert_messages.returning[0];
|
||||||
|
const broadcastRoom = getBodyshopRoom(insertedMessage.conversation.bodyshop.id);
|
||||||
|
const conversationRoom = getBodyshopConversationRoom({
|
||||||
|
bodyshopId: insertedMessage.conversation.bodyshop.id,
|
||||||
|
conversationId: insertedMessage.conversation.id
|
||||||
|
});
|
||||||
|
|
||||||
|
ioRedis.to(broadcastRoom).emit("new-message-summary", {
|
||||||
|
isoutbound: true,
|
||||||
|
conversationId: conversationid,
|
||||||
|
updated_at: insertedMessage.updated_at,
|
||||||
|
msid: message.sid,
|
||||||
|
summary: true
|
||||||
|
});
|
||||||
|
|
||||||
|
ioRedis.to(conversationRoom).emit("new-message-detailed", {
|
||||||
|
newMessage: insertedMessage,
|
||||||
|
conversationId: conversationid,
|
||||||
|
summary: false
|
||||||
|
});
|
||||||
|
|
||||||
|
res.sendStatus(200);
|
||||||
|
} catch (gqlError) {
|
||||||
|
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
||||||
|
msid: message.sid,
|
||||||
|
conversationid,
|
||||||
|
error: gqlError.message,
|
||||||
|
stack: gqlError.stack
|
||||||
|
});
|
||||||
|
res.status(500).json({ success: false, message: "Failed to insert message into database." });
|
||||||
|
}
|
||||||
|
} catch (twilioError) {
|
||||||
|
logger.log("sms-outbound-error", "ERROR", req.user.email, null, {
|
||||||
|
conversationid,
|
||||||
|
error: twilioError.message,
|
||||||
|
stack: twilioError.stack
|
||||||
|
});
|
||||||
|
res.status(500).json({ success: false, message: "Failed to send message through Twilio." });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,59 +5,102 @@ require("dotenv").config({
|
|||||||
|
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const { phone } = require("phone");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const { admin } = require("../firebase/firebase-handler");
|
|
||||||
|
|
||||||
exports.status = (req, res) => {
|
exports.status = async (req, res) => {
|
||||||
const { SmsSid, SmsStatus } = req.body;
|
const { SmsSid, SmsStatus } = req.body;
|
||||||
client
|
const {
|
||||||
.request(queries.UPDATE_MESSAGE_STATUS, {
|
ioRedis,
|
||||||
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ignore status 'queued'
|
||||||
|
if (SmsStatus === "queued") {
|
||||||
|
return res.status(200).json({ message: "Status 'queued' disregarded." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update message status in the database
|
||||||
|
const response = await client.request(queries.UPDATE_MESSAGE_STATUS, {
|
||||||
msid: SmsSid,
|
msid: SmsSid,
|
||||||
fields: { status: SmsStatus }
|
fields: { status: SmsStatus }
|
||||||
})
|
});
|
||||||
.then((response) => {
|
|
||||||
|
const message = response.update_messages.returning[0];
|
||||||
|
|
||||||
|
if (message) {
|
||||||
logger.log("sms-status-update", "DEBUG", "api", null, {
|
logger.log("sms-status-update", "DEBUG", "api", null, {
|
||||||
msid: SmsSid,
|
msid: SmsSid,
|
||||||
fields: { status: SmsStatus }
|
fields: { status: SmsStatus }
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.catch((error) => {
|
// Emit WebSocket event to notify the change in message status
|
||||||
logger.log("sms-status-update-error", "ERROR", "api", null, {
|
const conversationRoom = getBodyshopConversationRoom({
|
||||||
|
bodyshopId: message.conversation.bodyshopid,
|
||||||
|
conversationId: message.conversationid
|
||||||
|
});
|
||||||
|
|
||||||
|
ioRedis.to(conversationRoom).emit("message-changed", {
|
||||||
|
...message,
|
||||||
|
status: SmsStatus,
|
||||||
|
type: "status-changed"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.log("sms-status-update-warning", "WARN", "api", null, {
|
||||||
msid: SmsSid,
|
msid: SmsSid,
|
||||||
fields: { status: SmsStatus },
|
fields: { status: SmsStatus },
|
||||||
error
|
warning: "No message returned from the database update."
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(200);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("sms-status-update-error", "ERROR", "api", null, {
|
||||||
|
msid: SmsSid,
|
||||||
|
fields: { status: SmsStatus },
|
||||||
|
stack: error.stack,
|
||||||
|
message: error.message
|
||||||
});
|
});
|
||||||
res.sendStatus(200);
|
res.status(500).json({ error: "Failed to update message status." });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.markConversationRead = async (req, res) => {
|
exports.markConversationRead = async (req, res) => {
|
||||||
const { conversationid, imexshopid } = req.body;
|
const {
|
||||||
admin.messaging().send({
|
ioRedis,
|
||||||
topic: `${imexshopid}-messaging`,
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
// notification: {
|
} = req;
|
||||||
// title: `ImEX Online Message - ${data.phone_num}`,
|
const { conversation, imexshopid, bodyshopid } = req.body;
|
||||||
// body: message.image_path ? `Image ${message.text}` : message.text,
|
|
||||||
// imageUrl: "https://thinkimex.com/img/logo512.png",
|
|
||||||
// },
|
|
||||||
data: {
|
|
||||||
type: "messaging-mark-conversation-read",
|
|
||||||
conversationid: conversationid || ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
res.send(200);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Inbound Sample
|
// Alternatively, support both payload formats
|
||||||
// {
|
const conversationId = conversation?.id || req.body.conversationId;
|
||||||
// "SmsSid": "SM5205ea340e06437799d9345e7283457c",
|
|
||||||
// "SmsStatus": "queued",
|
if (!conversationId || !imexshopid || !bodyshopid) {
|
||||||
// "MessageStatus": "queued",
|
return res.status(400).json({ error: "Invalid conversation data provided." });
|
||||||
// "To": "+16049992002",
|
}
|
||||||
// "MessagingServiceSid": "MG6e259e2add04ffa0d0aa355038670ee1",
|
|
||||||
// "MessageSid": "SM5205ea340e06437799d9345e7283457c",
|
try {
|
||||||
// "AccountSid": "AC6c09d337d6b9c68ab6488c2052bd457c",
|
const response = await client.request(queries.MARK_MESSAGES_AS_READ, {
|
||||||
// "From": "+16043301606",
|
conversationId
|
||||||
// "ApiVersion": "2010-04-01"
|
});
|
||||||
// }
|
|
||||||
|
const updatedMessageIds = response.update_messages.returning.map((message) => message.id);
|
||||||
|
|
||||||
|
const broadcastRoom = getBodyshopRoom(bodyshopid);
|
||||||
|
|
||||||
|
ioRedis.to(broadcastRoom).emit("conversation-changed", {
|
||||||
|
type: "conversation-marked-read",
|
||||||
|
conversationId,
|
||||||
|
affectedMessages: response.update_messages.affected_rows,
|
||||||
|
messageIds: updatedMessageIds
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Conversation marked as read."
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error marking conversation as read:", error);
|
||||||
|
res.status(500).json({ error: "Failed to mark conversation as read." });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
26
server/utils/getHostNameOrIP.js
Normal file
26
server/utils/getHostNameOrIP.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Load environment variables THIS MUST BE AT THE TOP
|
||||||
|
const path = require("path");
|
||||||
|
require("dotenv").config({
|
||||||
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { networkInterfaces, hostname } = require("node:os");
|
||||||
|
|
||||||
|
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]) {
|
||||||
|
if (iface.family === "IPv4" && !iface.internal) {
|
||||||
|
return iface.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "127.0.0.1";
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = getHostNameOrIP;
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
const applyIOHelpers = ({ app, api, io, logger }) => {
|
const applyIOHelpers = ({ app, api, io, logger }) => {
|
||||||
const getBodyshopRoom = (bodyshopID) => `bodyshop-broadcast-room:${bodyshopID}`;
|
const getBodyshopRoom = (bodyshopID) => `bodyshop-broadcast-room:${bodyshopID}`;
|
||||||
|
// Messaging - conversation specific room to handle detailed messages when the user has a conversation open.
|
||||||
|
const getBodyshopConversationRoom = ({bodyshopId, conversationId}) =>
|
||||||
|
`bodyshop-conversation-room:${bodyshopId}:${conversationId}`;
|
||||||
|
|
||||||
const ioHelpersAPI = {
|
const ioHelpersAPI = {
|
||||||
getBodyshopRoom
|
getBodyshopRoom,
|
||||||
|
getBodyshopConversationRoom
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper middleware
|
// Helper middleware
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ const InstanceManager = require("../utils/instanceMgr").default;
|
|||||||
const winston = require("winston");
|
const winston = require("winston");
|
||||||
const WinstonCloudWatch = require("winston-cloudwatch");
|
const WinstonCloudWatch = require("winston-cloudwatch");
|
||||||
const { isString, isEmpty } = require("lodash");
|
const { isString, isEmpty } = require("lodash");
|
||||||
const { networkInterfaces, hostname } = require("node:os");
|
|
||||||
const { uploadFileToS3 } = require("./s3");
|
const { uploadFileToS3 } = require("./s3");
|
||||||
const { v4 } = require("uuid");
|
const { v4 } = require("uuid");
|
||||||
const { InstanceRegion } = require("./instanceMgr");
|
const { InstanceRegion } = require("./instanceMgr");
|
||||||
|
const getHostNameOrIP = require("./getHostNameOrIP");
|
||||||
|
|
||||||
const LOG_LEVELS = {
|
const LOG_LEVELS = {
|
||||||
error: { level: 0, name: "error" },
|
error: { level: 0, name: "error" },
|
||||||
@@ -76,22 +76,6 @@ const createLogger = () => {
|
|||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
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]) {
|
|
||||||
if (iface.family === "IPv4" && !iface.internal) {
|
|
||||||
return iface.address;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "127.0.0.1";
|
|
||||||
};
|
|
||||||
const createProductionTransport = (level, logStreamName, filters) => {
|
const createProductionTransport = (level, logStreamName, filters) => {
|
||||||
return new WinstonCloudWatch({
|
return new WinstonCloudWatch({
|
||||||
level,
|
level,
|
||||||
|
|||||||
52
server/utils/statusReporter.js
Normal file
52
server/utils/statusReporter.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Load environment variables THIS MUST BE AT THE TOP
|
||||||
|
const path = require("path");
|
||||||
|
const getHostNameOrIP = require("./getHostNameOrIP");
|
||||||
|
const logger = require("./logger");
|
||||||
|
require("dotenv").config({
|
||||||
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
|
});
|
||||||
|
const CrispStatusReporter = require("crisp-status-reporter").CrispStatusReporter;
|
||||||
|
const InstanceManager = require("../utils/instanceMgr").default;
|
||||||
|
|
||||||
|
function StartStatusReporter() {
|
||||||
|
//For ImEX Online.
|
||||||
|
|
||||||
|
return InstanceManager({
|
||||||
|
executeFunction: true,
|
||||||
|
args: [],
|
||||||
|
imex: () => {
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV === undefined ||
|
||||||
|
!process.env.CRISP_SECRET_TOKEN ||
|
||||||
|
!process.env.CRISP_SERVICE_IDENTIFIER ||
|
||||||
|
!process.env.CRISP_NODE_IDENTIFIER
|
||||||
|
) {
|
||||||
|
logger.log("crisp-status-update-error", "DEBUG", null, null, { message: "Environment Variables not set." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const crispStatusReporter = new CrispStatusReporter({
|
||||||
|
token: process.env.CRISP_SECRET_TOKEN, // Your reporter token (given by Crisp)
|
||||||
|
service_id: process.env.CRISP_SERVICE_IDENTIFIER, // Service ID containing the parent Node for Replica (given by Crisp)
|
||||||
|
node_id: process.env.CRISP_NODE_IDENTIFIER, // Node ID containing Replica (given by Crisp)
|
||||||
|
replica_id: getHostNameOrIP(), // Unique Replica ID for instance (ie. your IP on the LAN)
|
||||||
|
interval: 30 // Reporting interval (in seconds; defaults to 30 seconds if not set)
|
||||||
|
|
||||||
|
// console: {
|
||||||
|
// debug: (log_message, data) => logger.log("crisp-status-update", "DEBUG", null, null, { log_message, data }),
|
||||||
|
// log: (log_message, data) => logger.log("crisp-status-update", "DEBUG", null, null, { log_message, data }),
|
||||||
|
// warn: (log_message, data) => logger.log("crisp-status-update", "WARN", null, null, { log_message, data }),
|
||||||
|
// error: (log_message, data) => logger.log("crisp-status-update", "ERROR", null, null, { log_message, data })
|
||||||
|
// } // Console instance if you need to debug issues,
|
||||||
|
});
|
||||||
|
|
||||||
|
return crispStatusReporter;
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("crisp-status-update-error", "DEBUG", null, null, { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StartStatusReporter;
|
||||||
@@ -3,7 +3,7 @@ const { admin } = require("../firebase/firebase-handler");
|
|||||||
const redisSocketEvents = ({
|
const redisSocketEvents = ({
|
||||||
io,
|
io,
|
||||||
redisHelpers: { setSessionData, clearSessionData }, // Note: Used if we persist user to Redis
|
redisHelpers: { setSessionData, clearSessionData }, // Note: Used if we persist user to Redis
|
||||||
ioHelpers: { getBodyshopRoom },
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom },
|
||||||
logger
|
logger
|
||||||
}) => {
|
}) => {
|
||||||
// Logging helper functions
|
// Logging helper functions
|
||||||
@@ -46,18 +46,25 @@ const redisSocketEvents = ({
|
|||||||
|
|
||||||
// Token Update Events
|
// Token Update Events
|
||||||
const registerUpdateEvents = (socket) => {
|
const registerUpdateEvents = (socket) => {
|
||||||
|
let latestTokenTimestamp = 0;
|
||||||
|
|
||||||
const updateToken = async (newToken) => {
|
const updateToken = async (newToken) => {
|
||||||
|
const currentTimestamp = Date.now();
|
||||||
|
latestTokenTimestamp = currentTimestamp;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// noinspection UnnecessaryLocalVariableJS
|
// Verify token with Firebase Admin SDK
|
||||||
const user = await admin.auth().verifyIdToken(newToken, true);
|
const user = await admin.auth().verifyIdToken(newToken, true);
|
||||||
|
|
||||||
|
// Skip outdated token validations
|
||||||
|
if (currentTimestamp < latestTokenTimestamp) {
|
||||||
|
createLogEvent(socket, "warn", "Outdated token validation skipped.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
socket.user = user;
|
socket.user = user;
|
||||||
|
|
||||||
// If We ever want to persist user Data across workers
|
createLogEvent(socket, "debug", `Token updated successfully for socket ID: ${socket.id}`);
|
||||||
// await setSessionData(socket.id, "user", user);
|
|
||||||
|
|
||||||
// Uncomment for further testing
|
|
||||||
// createLogEvent(socket, "debug", "Token updated successfully");
|
|
||||||
|
|
||||||
socket.emit("token-updated", { success: true });
|
socket.emit("token-updated", { success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "auth/id-token-expired") {
|
if (error.code === "auth/id-token-expired") {
|
||||||
@@ -66,16 +73,20 @@ const redisSocketEvents = ({
|
|||||||
success: false,
|
success: false,
|
||||||
error: "Stale token."
|
error: "Stale token."
|
||||||
});
|
});
|
||||||
} else {
|
return; // Avoid disconnecting for expired tokens
|
||||||
createLogEvent(socket, "error", `Token update failed: ${error.message}`);
|
|
||||||
socket.emit("token-updated", { success: false, error: error.message });
|
|
||||||
// For any other errors, optionally disconnect the socket
|
|
||||||
socket.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createLogEvent(socket, "error", `Token update failed for socket ID: ${socket.id}, Error: ${error.message}`);
|
||||||
|
socket.emit("token-updated", { success: false, error: error.message });
|
||||||
|
|
||||||
|
// Optionally disconnect for invalid tokens or other errors
|
||||||
|
socket.disconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.on("update-token", updateToken);
|
socket.on("update-token", updateToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Room Broadcast Events
|
// Room Broadcast Events
|
||||||
const registerRoomAndBroadcastEvents = (socket) => {
|
const registerRoomAndBroadcastEvents = (socket) => {
|
||||||
const joinBodyshopRoom = (bodyshopUUID) => {
|
const joinBodyshopRoom = (bodyshopUUID) => {
|
||||||
@@ -113,6 +124,7 @@ const redisSocketEvents = ({
|
|||||||
socket.on("leave-bodyshop-room", leaveBodyshopRoom);
|
socket.on("leave-bodyshop-room", leaveBodyshopRoom);
|
||||||
socket.on("broadcast-to-bodyshop", broadcastToBodyshopRoom);
|
socket.on("broadcast-to-bodyshop", broadcastToBodyshopRoom);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disconnect Events
|
// Disconnect Events
|
||||||
const registerDisconnectEvents = (socket) => {
|
const registerDisconnectEvents = (socket) => {
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
@@ -130,9 +142,64 @@ const redisSocketEvents = ({
|
|||||||
socket.on("disconnect", disconnect);
|
socket.on("disconnect", disconnect);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Messaging Events
|
||||||
|
const registerMessagingEvents = (socket) => {
|
||||||
|
const joinConversationRoom = async ({ bodyshopId, conversationId }) => {
|
||||||
|
try {
|
||||||
|
const room = getBodyshopConversationRoom({ bodyshopId, conversationId });
|
||||||
|
socket.join(room);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("Failed to Join Conversation Room", "error", "io-redis", null, {
|
||||||
|
bodyshopId,
|
||||||
|
conversationId,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const leaveConversationRoom = ({ bodyshopId, conversationId }) => {
|
||||||
|
try {
|
||||||
|
const room = getBodyshopConversationRoom({ bodyshopId, conversationId });
|
||||||
|
socket.leave(room);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("Failed to Leave Conversation Room", "error", "io-redis", null, {
|
||||||
|
bodyshopId,
|
||||||
|
conversationId,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const conversationModified = ({ bodyshopId, conversationId, ...fields }) => {
|
||||||
|
try {
|
||||||
|
// Retrieve the room name for the conversation
|
||||||
|
const room = getBodyshopRoom(bodyshopId);
|
||||||
|
// Emit the updated data to all clients in the room
|
||||||
|
io.to(room).emit("conversation-changed", {
|
||||||
|
conversationId,
|
||||||
|
...fields
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("Failed to handle conversation modification", "error", "io-redis", null, {
|
||||||
|
bodyshopId,
|
||||||
|
conversationId,
|
||||||
|
fields,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("conversation-modified", conversationModified);
|
||||||
|
socket.on("join-bodyshop-conversation", joinConversationRoom);
|
||||||
|
socket.on("leave-bodyshop-conversation", leaveConversationRoom);
|
||||||
|
};
|
||||||
|
|
||||||
// Call Handlers
|
// Call Handlers
|
||||||
registerRoomAndBroadcastEvents(socket);
|
registerRoomAndBroadcastEvents(socket);
|
||||||
registerUpdateEvents(socket);
|
registerUpdateEvents(socket);
|
||||||
|
registerMessagingEvents(socket);
|
||||||
registerDisconnectEvents(socket);
|
registerDisconnectEvents(socket);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user