From ca3145ce0f54d1657eb0dc207a2823cea2b5acc0 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 19 Jan 2024 12:19:07 -0800 Subject: [PATCH 01/48] IO-2599 Remove Space and put Div in to space components --- .../production-list-detail.component.jsx | 154 +++++++++--------- 1 file changed, 79 insertions(+), 75 deletions(-) diff --git a/client/src/components/production-list-detail/production-list-detail.component.jsx b/client/src/components/production-list-detail/production-list-detail.component.jsx index 2963176b9..34265612b 100644 --- a/client/src/components/production-list-detail/production-list-detail.component.jsx +++ b/client/src/components/production-list-detail/production-list-detail.component.jsx @@ -105,85 +105,89 @@ export function ProductionListDetail({ {error && } {!loading && data && (
- - - - - - - {theJob.ro_number || ""} - - - - {data.jobs_by_pk.alt_transport || ""} - - - - - {theJob.clm_no || ""} - - - {theJob.ins_co_nm || ""} - - - - - {!technician ? ( - <> - - - - ) : ( - <> - - {data.jobs_by_pk.ownr_ph1} - - - {data.jobs_by_pk.ownr_ph2} - - - )} - - - - {`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${ - theJob.v_make_desc || "" - } ${theJob.v_model_desc || ""}`} - - - {theJob.clm_total} - - - {theJob.actual_in} - - - {theJob.scheduled_completion} - - - - - {!bodyshop.uselocalmediaserver && ( + + + +
+ + + {theJob.ro_number || ""} + + + + {data.jobs_by_pk.alt_transport || ""} + + + + + {theJob.clm_no || ""} + + + {theJob.ins_co_nm || ""} + + + + + {!technician ? ( + <> + + + + ) : ( + <> + + {data.jobs_by_pk.ownr_ph1} + + + {data.jobs_by_pk.ownr_ph2} + + + )} + + + + {`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${ + theJob.v_make_desc || "" + } ${theJob.v_model_desc || ""}`} + + + {theJob.clm_total} + + + {theJob.actual_in} + + + {theJob.scheduled_completion} + + +
+ +
+ + {!bodyshop.uselocalmediaserver && ( + <> +
- )} - + + )}
)} From c9cbffdec8df7e1d3316f2e118aee4448fe3c0b3 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 19 Jan 2024 13:15:06 -0800 Subject: [PATCH 02/48] IO-2543 Schema changes for AR calculation. --- client/package-lock.json | 13 -- client/yarn.lock | 136 ++++++++++-------- hasura/metadata/functions.yaml | 3 + hasura/metadata/tables.yaml | 5 + .../1705693552101_run_sql_migration/down.sql | 33 +++++ .../1705693552101_run_sql_migration/up.sql | 31 ++++ .../down.sql | 1 + .../up.sql | 1 + .../1705693896379_run_sql_migration/down.sql | 34 +++++ .../1705693896379_run_sql_migration/up.sql | 32 +++++ .../1705694146809_run_sql_migration/down.sql | 32 +++++ .../1705694146809_run_sql_migration/up.sql | 30 ++++ .../1705694176838_run_sql_migration/down.sql | 32 +++++ .../1705694176838_run_sql_migration/up.sql | 30 ++++ .../1705696451631_run_sql_migration/down.sql | 32 +++++ .../1705696451631_run_sql_migration/up.sql | 30 ++++ .../down.sql | 4 + .../up.sql | 2 + .../1705698426997_run_sql_migration/down.sql | 33 +++++ .../1705698426997_run_sql_migration/up.sql | 31 ++++ .../down.sql | 4 + .../up.sql | 2 + .../1705698534883_run_sql_migration/down.sql | 34 +++++ .../1705698534883_run_sql_migration/up.sql | 32 +++++ .../1705698593644_run_sql_migration/down.sql | 34 +++++ .../1705698593644_run_sql_migration/up.sql | 32 +++++ .../1705698876975_run_sql_migration/down.sql | 34 +++++ .../1705698876975_run_sql_migration/up.sql | 32 +++++ package-lock.json | 2 +- 29 files changed, 677 insertions(+), 74 deletions(-) create mode 100644 hasura/migrations/1705693552101_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705693552101_run_sql_migration/up.sql create mode 100644 hasura/migrations/1705693852612_create_table_public_job_ar_schema/down.sql create mode 100644 hasura/migrations/1705693852612_create_table_public_job_ar_schema/up.sql create mode 100644 hasura/migrations/1705693896379_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705693896379_run_sql_migration/up.sql create mode 100644 hasura/migrations/1705694146809_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705694146809_run_sql_migration/up.sql create mode 100644 hasura/migrations/1705694176838_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705694176838_run_sql_migration/up.sql create mode 100644 hasura/migrations/1705696451631_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705696451631_run_sql_migration/up.sql create mode 100644 hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/down.sql create mode 100644 hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/up.sql create mode 100644 hasura/migrations/1705698426997_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705698426997_run_sql_migration/up.sql create mode 100644 hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/down.sql create mode 100644 hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/up.sql create mode 100644 hasura/migrations/1705698534883_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705698534883_run_sql_migration/up.sql create mode 100644 hasura/migrations/1705698593644_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705698593644_run_sql_migration/up.sql create mode 100644 hasura/migrations/1705698876975_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705698876975_run_sql_migration/up.sql diff --git a/client/package-lock.json b/client/package-lock.json index 0409aecde..91b98ad66 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -22110,19 +22110,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/typescript-compare": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", diff --git a/client/yarn.lock b/client/yarn.lock index ce49d3489..8aeb33e0e 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -53,7 +53,7 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@apollo/client@^3.0.0", "@apollo/client@^3.7.9": +"@apollo/client@^3.7.9": version "3.7.9" resolved "https://registry.npmjs.org/@apollo/client/-/client-3.7.9.tgz" integrity sha512-YnJvrJOVWrp4y/zdNvUaM8q4GuSHCEIecsRDTJhK/veT33P/B7lfqGJ24NeLdKMj8tDEuXYF7V0t+th4+rgC+Q== @@ -96,7 +96,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz" integrity sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.1.0", "@babel/core@^7.12.0", "@babel/core@^7.12.3", "@babel/core@^7.13.0", "@babel/core@^7.16.0", "@babel/core@^7.4.0-0", "@babel/core@^7.8.0", "@babel/core@>=7.11.0": +"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.16.0": version "7.18.9" resolved "https://registry.npmjs.org/@babel/core/-/core-7.18.9.tgz" integrity sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g== @@ -159,6 +159,27 @@ json5 "^2.2.2" semver "^6.3.0" +"@babel/core@^7.8.0": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.21.0.tgz" + integrity sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.21.0" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-module-transforms" "^7.21.0" + "@babel/helpers" "^7.21.0" + "@babel/parser" "^7.21.0" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.0" + "@babel/types" "^7.21.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + "@babel/eslint-parser@^7.16.3": version "7.19.1" resolved "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz" @@ -729,7 +750,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-flow@^7.14.5", "@babel/plugin-syntax-flow@^7.18.6": +"@babel/plugin-syntax-flow@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz" integrity sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A== @@ -1127,7 +1148,7 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.18.6" -"@babel/plugin-transform-react-jsx@^7.14.9", "@babel/plugin-transform-react-jsx@^7.18.6": +"@babel/plugin-transform-react-jsx@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz" integrity sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw== @@ -1568,7 +1589,7 @@ resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@craco/craco@^6.0.0", "@craco/craco@^7.0.0": +"@craco/craco@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/@craco/craco/-/craco-7.0.0.tgz" integrity sha512-OyjL9zpURB6Ha1HO62Hlt27Xd7UYJ8DRiBNuE4DBB8Ue0iQ9q/xsv3ze7ROm6gCZqV6I2Gxjnq0EHCCye+4xDQ== @@ -1844,7 +1865,7 @@ "@firebase/util" "1.9.2" tslib "^2.1.0" -"@firebase/app-compat@0.2.3", "@firebase/app-compat@0.x": +"@firebase/app-compat@0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.3.tgz" integrity sha512-sX6rD1KFX6K2CuCnQvc9jZLOgAFZ+sv2jKKahIl4SbTM561D682B8n4Jtx/SgDrvcTVTdb05g4NhZOws9hxYxA== @@ -1855,12 +1876,12 @@ "@firebase/util" "1.9.2" tslib "^2.1.0" -"@firebase/app-types@0.9.0", "@firebase/app-types@0.x": +"@firebase/app-types@0.9.0": version "0.9.0" resolved "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz" integrity sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q== -"@firebase/app@0.9.3", "@firebase/app@0.x": +"@firebase/app@0.9.3": version "0.9.3" resolved "https://registry.npmjs.org/@firebase/app/-/app-0.9.3.tgz" integrity sha512-G79JUceVDaHRZ4WkA11GyVldVXhdyRJRwWVQFFvAAVfQJLvy2TA6lQjeUn28F6FmeUWxDGwPC30bxCRWq7Op8Q== @@ -2145,7 +2166,7 @@ node-fetch "2.6.7" tslib "^2.1.0" -"@firebase/util@1.9.2", "@firebase/util@1.x": +"@firebase/util@1.9.2": version "1.9.2" resolved "https://registry.npmjs.org/@firebase/util/-/util-1.9.2.tgz" integrity sha512-9l0uMGPGw3GsoD5khjMmYCCcMq/OR/OOSViiWMN+s2Q0pxM+fYzrii1H+r8qC/uoMjSVXomjLZt0vZIyryCqtQ== @@ -3050,7 +3071,7 @@ resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz" integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.12", "@types/babel__core@^7.1.9": +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.12": version "7.1.19" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz" integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== @@ -3358,7 +3379,7 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react@*", "@types/react@^16.8 || ^17.0 || ^18.0", "@types/react@>=16.9.11": +"@types/react@*", "@types/react@>=16.9.11": version "18.0.15" resolved "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz" integrity sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow== @@ -3474,7 +3495,7 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^4.0.0 || ^5.0.0", "@typescript-eslint/eslint-plugin@^5.5.0": +"@typescript-eslint/eslint-plugin@^5.5.0": version "5.54.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.54.0.tgz" integrity sha512-+hSN9BdSr629RF02d7mMtXhAJvDTyCbprNYJKrXETlul/Aml6YZwd90XioVbjejQeHbb3R8Dg0CkRgoJDxo8aw== @@ -3497,7 +3518,7 @@ dependencies: "@typescript-eslint/utils" "5.54.0" -"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.5.0": +"@typescript-eslint/parser@^5.5.0": version "5.54.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.54.0.tgz" integrity sha512-aAVL3Mu2qTi+h/r04WI/5PfNWvO6pdhpeMRWk9R7rEV4mwJNzoWf5CCU5vDKBsPIFQFjEq1xg7XBI2rjiMXQbQ== @@ -3779,11 +3800,6 @@ acorn-walk@^8.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: - version "8.8.2" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - acorn@^7.0.0: version "7.4.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" @@ -3794,6 +3810,11 @@ acorn@^7.1.1: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: + version "8.8.2" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + address@^1.0.1, address@^1.1.2: version "1.2.2" resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz" @@ -3841,7 +3862,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, ajv@^6.9.1: +ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3851,7 +3872,7 @@ ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.0, ajv@^8.8.2: +ajv@^8.0.0, ajv@^8.8.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -3861,7 +3882,7 @@ ajv@^8.0.0, ajv@^8.8.0, ajv@^8.8.2: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^8.6.0, ajv@>=8: +ajv@^8.6.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -4561,7 +4582,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.21.2, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5, "browserslist@>= 4", "browserslist@>= 4.21.0", browserslist@>=4: +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.21.2, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5: version "4.21.5" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== @@ -5363,7 +5384,7 @@ cuint@^0.2.2: resolved "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz" integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw== -cypress@^10.3.1, "cypress@^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0": +cypress@^10.3.1: version "10.11.0" resolved "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz" integrity sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA== @@ -6020,7 +6041,7 @@ enquire.js@^2.1.6: resolved "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz" integrity sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw== -enquirer@^2.3.6, "enquirer@>= 2.3.0 < 3": +enquirer@^2.3.6: version "2.3.6" resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -6370,7 +6391,7 @@ eslint-webpack-plugin@^3.1.1: normalize-path "^3.0.0" schema-utils "^4.0.0" -eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^7.0.0 || ^8.0.0", "eslint@^7.5.0 || ^8.0.0", eslint@^8.0.0, eslint@^8.1.0, eslint@^8.3.0, "eslint@>= 3.2.1", "eslint@>= 6", eslint@>=5: +eslint@^8.3.0: version "8.35.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz" integrity sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw== @@ -7150,7 +7171,7 @@ graphql-tag@^2.12.6: dependencies: tslib "^2.1.0" -"graphql@^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^15.7.2 || ^16.0.0", graphql@^16.6.0: +graphql@^16.6.0: version "16.6.0" resolved "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz" integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== @@ -7414,7 +7435,7 @@ i18next-browser-languagedetector@^7.0.1: dependencies: "@babel/runtime" "^7.19.4" -i18next@^22.4.10, "i18next@>= 19.0.0": +i18next@^22.4.10: version "22.4.10" resolved "https://registry.npmjs.org/i18next/-/i18next-22.4.10.tgz" integrity sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA== @@ -8247,7 +8268,7 @@ jest-resolve-dependencies@^27.5.1: jest-regex-util "^27.5.1" jest-snapshot "^27.5.1" -jest-resolve@*, jest-resolve@^27.4.2, jest-resolve@^27.5.1: +jest-resolve@^27.4.2, jest-resolve@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz" integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== @@ -8457,7 +8478,7 @@ jest-worker@^28.0.2: merge-stream "^2.0.0" supports-color "^8.0.0" -"jest@^27.0.0 || ^28.0.0", jest@^27.4.3: +jest@^27.4.3: version "27.5.1" resolved "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz" integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== @@ -8724,7 +8745,7 @@ less-loader@^7.3.0: loader-utils "^2.0.0" schema-utils "^3.0.0" -"less@^3.5.0 || ^4.0.0", less@^4.1.1: +less@^4.1.1: version "4.1.3" resolved "https://registry.npmjs.org/less/-/less-4.1.3.tgz" integrity sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA== @@ -9186,7 +9207,7 @@ moment-timezone@^0.5.40, moment-timezone@^0.5.41: dependencies: moment "^2.29.4" -moment@^2.24.0, moment@^2.29.2, moment@^2.29.4, moment@2.x.x: +moment@^2.24.0, moment@^2.29.2, moment@^2.29.4: version "2.29.4" resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== @@ -10298,15 +10319,6 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -"postcss@^7.0.0 || ^8.0.1", postcss@^8, postcss@^8.0.0, postcss@^8.0.3, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.1.4, postcss@^8.2, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.3, postcss@^8.3.5, postcss@^8.4, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.4, postcss@^8.4.6, "postcss@>= 8", postcss@>=8, postcss@>=8.0.9: - version "8.4.21" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz" - integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== - dependencies: - nanoid "^3.3.4" - picocolors "^1.0.0" - source-map-js "^1.0.2" - postcss@^7.0.35: version "7.0.39" resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" @@ -10315,6 +10327,15 @@ postcss@^7.0.35: picocolors "^0.2.1" source-map "^0.6.1" +postcss@^8.0.9, postcss@^8.3.5, postcss@^8.4.19, postcss@^8.4.4: + version "8.4.21" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -10389,7 +10410,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1, prop-types@15.x: +prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1, prop-types@15.x: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -11119,7 +11140,7 @@ react-dev-utils@^12.0.1: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@*, "react-dom@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0", "react-dom@^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", "react-dom@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^15.3.0 || ^16.0.0-alpha", "react-dom@^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.14.0 || ^17 || ^18", "react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^16.8.0 || ^17.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.5 || ^17.0.0", react-dom@^17.0.2, "react-dom@>= 16.3.0", "react-dom@>= 16.8.0", react-dom@>=15, react-dom@>=15.0.0, react-dom@>=16.0.0, react-dom@>=16.11.0, react-dom@>=16.3.0, react-dom@>=16.9.0, "react-dom@16.x || 17.x": +react-dom@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== @@ -11191,7 +11212,7 @@ react-intersection-observer@^9.4.3: resolved "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.3.tgz" integrity sha512-WNRqMQvKpupr6MzecAQI0Pj0+JQong307knLP4g/nBex7kYfIaZsPpXaIhKHR+oV8z+goUbH9e10j6lGRnTzlQ== -react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, "react-is@>= 16.8.0": +react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -11271,7 +11292,7 @@ react-redux@^8.0.5: react-is "^18.0.0" use-sync-external-store "^1.0.0" -react-refresh@^0.11.0, "react-refresh@>=0.10.0 <1.0.0": +react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== @@ -11320,7 +11341,7 @@ react-router@5.3.3: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-scripts@^5.0.0, react-scripts@^5.0.1: +react-scripts@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz" integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ== @@ -11421,7 +11442,7 @@ react-virtualized@^9.22.3: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" -react@*, "react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0", "react@^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", "react@^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "react@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^15.3.0 || ^16.0.0-alpha", "react@^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.14.0 || ^17 || ^18", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8.0 || ^17.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.3 || ^17 || ^18", "react@^16.8.5 || ^17.0.0", react@^17.0.2, "react@>= 16", "react@>= 16.3", "react@>= 16.3.0", "react@>= 16.8.0", react@>=15, react@>=15.0.0, react@>=16.0.0, react@>=16.11.0, react@>=16.14.0, react@>=16.3.0, react@>=16.8.0, react@>=16.9.0, "react@15.x || 16.x || 17.x || 18.x", "react@16.x || 17.x", react@17.0.2: +react@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== @@ -11552,7 +11573,7 @@ redux-state-sync@^3.1.4: dependencies: broadcast-channel "^3.1.0" -redux@^4, redux@^4.0.0, redux@^4.0.4, redux@^4.2.1, redux@>4.0.0: +redux@^4.0.0, redux@^4.0.4, redux@^4.2.1: version "4.2.1" resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -11818,7 +11839,7 @@ rollup-plugin-terser@^7.0.0: serialize-javascript "^4.0.0" terser "^5.0.0" -"rollup@^1.20.0 || ^2.0.0", rollup@^1.20.0||^2.0.0, rollup@^2.0.0, rollup@^2.43.1: +rollup@^2.43.1: version "2.79.1" resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz" integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== @@ -11886,7 +11907,7 @@ sass-loader@^12.3.0: klona "^2.0.4" neo-async "^2.6.2" -sass@^1.3.0, sass@^1.58.3: +sass@^1.58.3: version "1.58.3" resolved "https://registry.npmjs.org/sass/-/sass-1.58.3.tgz" integrity sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A== @@ -12517,7 +12538,7 @@ style-utils@~0.2.0: resolved "https://registry.npmjs.org/style-utils/-/style-utils-0.2.1.tgz" integrity sha512-eKRIfWnUSdBqe2ko+qisUwBSlfWpHru89geRqzmScpDhkPW1ksmE04d//nDcXeF+TVK5cnBG90mMmHgxyxXleQ== -styled-components@^5.3.6, "styled-components@>= 2": +styled-components@^5.3.6: version "5.3.6" resolved "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz" integrity sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg== @@ -12541,7 +12562,7 @@ stylehacks@^5.1.1: browserslist "^4.21.4" postcss-selector-parser "^6.0.4" -subscriptions-transport-ws@^0.11.0, "subscriptions-transport-ws@^0.9.0 || ^0.11.0": +subscriptions-transport-ws@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz" integrity sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ== @@ -12896,7 +12917,7 @@ ts-invariant@^0.10.3: dependencies: tslib "^2.1.0" -ts-node@^10.7.0, ts-node@>=9.0.0: +ts-node@^10.7.0: version "10.9.1" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz" integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== @@ -13007,7 +13028,7 @@ type-fest@^0.16.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz" integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== -type-fest@^0.20.2, "type-fest@>=0.17.0 <4.0.0": +type-fest@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== @@ -13060,11 +13081,6 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -"typescript@^3.2.1 || ^4", "typescript@>= 2.7", typescript@>=2.7, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@>=3: - version "4.9.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" @@ -13370,7 +13386,7 @@ webpack-dev-middleware@^5.3.1: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@^4.6.0, "webpack-dev-server@3.x || 4.x": +webpack-dev-server@^4.6.0: version "4.11.1" resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz" integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw== @@ -13442,7 +13458,7 @@ webpack-sources@^2.2.0: source-list-map "^2.0.1" source-map "^0.6.1" -"webpack@^4.0.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", "webpack@^4.4.0 || ^5.9.0", "webpack@^4.44.2 || ^5.47.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.64.4, "webpack@>= 4", webpack@>=2, "webpack@>=4.43.0 <6.0.0": +webpack@^5.64.4: version "5.75.0" resolved "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz" integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ== diff --git a/hasura/metadata/functions.yaml b/hasura/metadata/functions.yaml index 3ef10184a..5dd1b8640 100644 --- a/hasura/metadata/functions.yaml +++ b/hasura/metadata/functions.yaml @@ -1,3 +1,6 @@ +- function: + name: jobs_ar_summary + schema: public - function: name: search_bills schema: public diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index f4e4d99a7..fda2aa44a 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -2673,6 +2673,9 @@ - table: name: ioevents schema: public +- table: + name: job_ar_schema + schema: public - table: name: job_conversations schema: public @@ -3812,6 +3815,7 @@ - referral_source - referral_source_extra - regie_number + - remove_from_ar - ro_number - scheduled_completion - scheduled_delivery @@ -4093,6 +4097,7 @@ - referral_source - referral_source_extra - regie_number + - remove_from_ar - ro_number - scheduled_completion - scheduled_delivery diff --git a/hasura/migrations/1705693552101_run_sql_migration/down.sql b/hasura/migrations/1705693552101_run_sql_migration/down.sql new file mode 100644 index 000000000..8fd036e68 --- /dev/null +++ b/hasura/migrations/1705693552101_run_sql_migration/down.sql @@ -0,0 +1,33 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF jobs +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.shopid, +-- j.ro_number, +-- j.clm_total, +-- p.total_payments, +-- j.clm_total - p.total_payments as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- sum(p.amount) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705693552101_run_sql_migration/up.sql b/hasura/migrations/1705693552101_run_sql_migration/up.sql new file mode 100644 index 000000000..56f2ff938 --- /dev/null +++ b/hasura/migrations/1705693552101_run_sql_migration/up.sql @@ -0,0 +1,31 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF jobs + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.shopid, + j.ro_number, + j.clm_total, + p.total_payments, + j.clm_total - p.total_payments as balance +from + jobs j +left join ( + select + p.jobid, + sum(p.amount) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705693852612_create_table_public_job_ar_schema/down.sql b/hasura/migrations/1705693852612_create_table_public_job_ar_schema/down.sql new file mode 100644 index 000000000..1ef08bca9 --- /dev/null +++ b/hasura/migrations/1705693852612_create_table_public_job_ar_schema/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."job_ar_schema"; diff --git a/hasura/migrations/1705693852612_create_table_public_job_ar_schema/up.sql b/hasura/migrations/1705693852612_create_table_public_job_ar_schema/up.sql new file mode 100644 index 000000000..fe574a140 --- /dev/null +++ b/hasura/migrations/1705693852612_create_table_public_job_ar_schema/up.sql @@ -0,0 +1 @@ +CREATE TABLE "public"."job_ar_schema" ("id" uuid NOT NULL, "ro_number" text, "clm_total" numeric NOT NULL, "total_payments" numeric NOT NULL DEFAULT 0, "balance" numeric NOT NULL DEFAULT 0, PRIMARY KEY ("id") ); diff --git a/hasura/migrations/1705693896379_run_sql_migration/down.sql b/hasura/migrations/1705693896379_run_sql_migration/down.sql new file mode 100644 index 000000000..a180c91a5 --- /dev/null +++ b/hasura/migrations/1705693896379_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- DROP FUNCTION public.jobs_ar_summary; +-- +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- p.total_payments, +-- j.clm_total - p.total_payments as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- sum(p.amount) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705693896379_run_sql_migration/up.sql b/hasura/migrations/1705693896379_run_sql_migration/up.sql new file mode 100644 index 000000000..48c04c38f --- /dev/null +++ b/hasura/migrations/1705693896379_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +DROP FUNCTION public.jobs_ar_summary; + +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + p.total_payments, + j.clm_total - p.total_payments as balance +from + jobs j +left join ( + select + p.jobid, + sum(p.amount) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705694146809_run_sql_migration/down.sql b/hasura/migrations/1705694146809_run_sql_migration/down.sql new file mode 100644 index 000000000..0a84cd97e --- /dev/null +++ b/hasura/migrations/1705694146809_run_sql_migration/down.sql @@ -0,0 +1,32 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- p.total_payments, +-- j.clm_total - p.total_payments as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705694146809_run_sql_migration/up.sql b/hasura/migrations/1705694146809_run_sql_migration/up.sql new file mode 100644 index 000000000..8b6779a91 --- /dev/null +++ b/hasura/migrations/1705694146809_run_sql_migration/up.sql @@ -0,0 +1,30 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + p.total_payments, + j.clm_total - p.total_payments as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705694176838_run_sql_migration/down.sql b/hasura/migrations/1705694176838_run_sql_migration/down.sql new file mode 100644 index 000000000..0a84cd97e --- /dev/null +++ b/hasura/migrations/1705694176838_run_sql_migration/down.sql @@ -0,0 +1,32 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- p.total_payments, +-- j.clm_total - p.total_payments as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705694176838_run_sql_migration/up.sql b/hasura/migrations/1705694176838_run_sql_migration/up.sql new file mode 100644 index 000000000..8b6779a91 --- /dev/null +++ b/hasura/migrations/1705694176838_run_sql_migration/up.sql @@ -0,0 +1,30 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + p.total_payments, + j.clm_total - p.total_payments as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705696451631_run_sql_migration/down.sql b/hasura/migrations/1705696451631_run_sql_migration/down.sql new file mode 100644 index 000000000..0826360f9 --- /dev/null +++ b/hasura/migrations/1705696451631_run_sql_migration/down.sql @@ -0,0 +1,32 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705696451631_run_sql_migration/up.sql b/hasura/migrations/1705696451631_run_sql_migration/up.sql new file mode 100644 index 000000000..a78f18383 --- /dev/null +++ b/hasura/migrations/1705696451631_run_sql_migration/up.sql @@ -0,0 +1,30 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/down.sql b/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/down.sql new file mode 100644 index 000000000..676aeaafd --- /dev/null +++ b/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."jobs" add column "remove_from_ar" boolean +-- not null default 'false'; diff --git a/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/up.sql b/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/up.sql new file mode 100644 index 000000000..562618ab9 --- /dev/null +++ b/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/up.sql @@ -0,0 +1,2 @@ +alter table "public"."jobs" add column "remove_from_ar" boolean + not null default 'false'; diff --git a/hasura/migrations/1705698426997_run_sql_migration/down.sql b/hasura/migrations/1705698426997_run_sql_migration/down.sql new file mode 100644 index 000000000..c564b5e7c --- /dev/null +++ b/hasura/migrations/1705698426997_run_sql_migration/down.sql @@ -0,0 +1,33 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705698426997_run_sql_migration/up.sql b/hasura/migrations/1705698426997_run_sql_migration/up.sql new file mode 100644 index 000000000..f1854fcac --- /dev/null +++ b/hasura/migrations/1705698426997_run_sql_migration/up.sql @@ -0,0 +1,31 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false; + + +END +$function$; diff --git a/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/down.sql b/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/down.sql new file mode 100644 index 000000000..aff5d0afe --- /dev/null +++ b/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."job_ar_schema" add column "date_invoiced" timestamptz +-- null; diff --git a/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/up.sql b/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/up.sql new file mode 100644 index 000000000..bd8f477ec --- /dev/null +++ b/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/up.sql @@ -0,0 +1,2 @@ +alter table "public"."job_ar_schema" add column "date_invoiced" timestamptz + null; diff --git a/hasura/migrations/1705698534883_run_sql_migration/down.sql b/hasura/migrations/1705698534883_run_sql_migration/down.sql new file mode 100644 index 000000000..a68a79058 --- /dev/null +++ b/hasura/migrations/1705698534883_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- j.date_invoiced, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705698534883_run_sql_migration/up.sql b/hasura/migrations/1705698534883_run_sql_migration/up.sql new file mode 100644 index 000000000..9961003f7 --- /dev/null +++ b/hasura/migrations/1705698534883_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + j.date_invoiced, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null; + + +END +$function$; diff --git a/hasura/migrations/1705698593644_run_sql_migration/down.sql b/hasura/migrations/1705698593644_run_sql_migration/down.sql new file mode 100644 index 000000000..c11116ffb --- /dev/null +++ b/hasura/migrations/1705698593644_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705698593644_run_sql_migration/up.sql b/hasura/migrations/1705698593644_run_sql_migration/up.sql new file mode 100644 index 000000000..729ee69d9 --- /dev/null +++ b/hasura/migrations/1705698593644_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null; + + +END +$function$; diff --git a/hasura/migrations/1705698876975_run_sql_migration/down.sql b/hasura/migrations/1705698876975_run_sql_migration/down.sql new file mode 100644 index 000000000..190511742 --- /dev/null +++ b/hasura/migrations/1705698876975_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and balance > 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705698876975_run_sql_migration/up.sql b/hasura/migrations/1705698876975_run_sql_migration/up.sql new file mode 100644 index 000000000..4979c0a5e --- /dev/null +++ b/hasura/migrations/1705698876975_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and balance > 0; + + +END +$function$; diff --git a/package-lock.json b/package-lock.json index a4e02c21b..6658c405a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "source-map-explorer": "^2.5.2" }, "engines": { - "node": ">=16.0.0", + "node": ">=18.0.0", "npm": ">=8.0.0" } }, From 9b4c85c9e3669d4505b7084306921559e01f3aa5 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 19 Jan 2024 13:50:14 -0800 Subject: [PATCH 03/48] IO-2543 additional schema correction. --- .../1705700945994_run_sql_migration/down.sql | 34 +++++++++++++++++++ .../1705700945994_run_sql_migration/up.sql | 32 +++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 hasura/migrations/1705700945994_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705700945994_run_sql_migration/up.sql diff --git a/hasura/migrations/1705700945994_run_sql_migration/down.sql b/hasura/migrations/1705700945994_run_sql_migration/down.sql new file mode 100644 index 000000000..d1ca7dbe0 --- /dev/null +++ b/hasura/migrations/1705700945994_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705700945994_run_sql_migration/up.sql b/hasura/migrations/1705700945994_run_sql_migration/up.sql new file mode 100644 index 000000000..25c2b3e1e --- /dev/null +++ b/hasura/migrations/1705700945994_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0; + + +END +$function$; From 166efdc877060764f3196ce730a1c575b38ef95b Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 22 Jan 2024 14:25:21 -0500 Subject: [PATCH 04/48] progressUpdate Signed-off-by: Dave Richer --- hasura/metadata/tables.yaml | 11 ++++++++--- server/job/job-status-transition.js | 10 ++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index fda2aa44a..46a1999a1 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -2442,7 +2442,7 @@ _eq: X-Hasura-User-Id columns: - address - - buisness_name + - business_name - date_accepted - eulaid - first_name @@ -2454,7 +2454,7 @@ permission: columns: - address - - buisness_name + - business_name - first_name - last_name - phone_number @@ -4164,11 +4164,16 @@ - name: job_status_transition definition: enable_manual: true + insert: + columns: '*' + update: + columns: + - status retry_conf: interval_sec: 10 num_retries: 0 timeout_sec: 60 - webhook_from_env: HASURA_API_URL + webhook: https://worktest.home.irony.online headers: - name: event-secret value_from_env: EVENT_SECRET diff --git a/server/job/job-status-transition.js b/server/job/job-status-transition.js index fd74a2ec3..294ab395d 100644 --- a/server/job/job-status-transition.js +++ b/server/job/job-status-transition.js @@ -20,13 +20,19 @@ async function StatusTransition(req, res) { res.status(401).send("Unauthorized"); return; } - res.sendStatus(200); - return; + + // return res.sendStatus(200); + const { id: jobid, status: value, shopid: bodyshopid, } = req.body.event.data.new; + + // Create record OPEN on new item, enter state + // If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status + // (Timeline) + // Final status is exported, there is no end date as there is no further transition (has no end date) try { const { update_transitions } = await client.request( queries.UPDATE_OLD_TRANSITION, From d61ed03ef1f85ba7c8962350dbc47a283f3cee4d Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 22 Jan 2024 11:52:42 -0800 Subject: [PATCH 05/48] Merge in EULA changes & update to AR Tracking schema. --- hasura/metadata/tables.yaml | 36 +++++++++++++++++-- .../down.sql | 1 + .../up.sql | 1 + .../down.sql | 1 + .../up.sql | 1 + .../down.sql | 1 + .../up.sql | 1 + .../down.sql | 4 +++ .../up.sql | 2 ++ .../down.sql | 1 + .../up.sql | 5 +++ .../1705952926623_run_sql_migration/down.sql | 35 ++++++++++++++++++ .../1705952926623_run_sql_migration/up.sql | 33 +++++++++++++++++ 13 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/down.sql create mode 100644 hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/up.sql create mode 100644 hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/down.sql create mode 100644 hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/up.sql create mode 100644 hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/down.sql create mode 100644 hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/up.sql create mode 100644 hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/down.sql create mode 100644 hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/up.sql create mode 100644 hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/down.sql create mode 100644 hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/up.sql create mode 100644 hasura/migrations/1705952926623_run_sql_migration/down.sql create mode 100644 hasura/migrations/1705952926623_run_sql_migration/up.sql diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index fda2aa44a..b9efbb437 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -2442,7 +2442,7 @@ _eq: X-Hasura-User-Id columns: - address - - buisness_name + - business_name - date_accepted - eulaid - first_name @@ -2454,7 +2454,7 @@ permission: columns: - address - - buisness_name + - business_name - first_name - last_name - phone_number @@ -2676,6 +2676,31 @@ - table: name: job_ar_schema schema: public + object_relationships: + - name: job + using: + foreign_key_constraint_on: id + select_permissions: + - role: user + permission: + columns: + - id + - ro_number + - clm_total + - total_payments + - balance + - date_invoiced + - shopid + filter: + job: + bodyshop: + associations: + _and: + - active: + _eq: true + - user: + authid: + _eq: X-Hasura-User-Id - table: name: job_conversations schema: public @@ -4164,11 +4189,16 @@ - name: job_status_transition definition: enable_manual: true + insert: + columns: '*' + update: + columns: + - status retry_conf: interval_sec: 10 num_retries: 0 timeout_sec: 60 - webhook_from_env: HASURA_API_URL + webhook: https://worktest.home.irony.online headers: - name: event-secret value_from_env: EVENT_SECRET diff --git a/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/down.sql b/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/down.sql new file mode 100644 index 000000000..8c4d38542 --- /dev/null +++ b/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/down.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" rename column "business_name" to "buisness_name"; diff --git a/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/up.sql b/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/up.sql new file mode 100644 index 000000000..7dc59b9f6 --- /dev/null +++ b/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/up.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" rename column "buisness_name" to "business_name"; diff --git a/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/down.sql b/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/down.sql new file mode 100644 index 000000000..08ab66099 --- /dev/null +++ b/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/down.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" alter column "phone_number" set not null; diff --git a/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/up.sql b/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/up.sql new file mode 100644 index 000000000..de5721802 --- /dev/null +++ b/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/up.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" alter column "phone_number" drop not null; diff --git a/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/down.sql b/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/down.sql new file mode 100644 index 000000000..fa66ebd58 --- /dev/null +++ b/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/down.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" alter column "address" set not null; diff --git a/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/up.sql b/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/up.sql new file mode 100644 index 000000000..d571c7cf3 --- /dev/null +++ b/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/up.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" alter column "address" drop not null; diff --git a/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/down.sql b/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/down.sql new file mode 100644 index 000000000..fe15ee344 --- /dev/null +++ b/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."job_ar_schema" add column "shopid" uuid +-- null; diff --git a/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/up.sql b/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/up.sql new file mode 100644 index 000000000..349ad38de --- /dev/null +++ b/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/up.sql @@ -0,0 +1,2 @@ +alter table "public"."job_ar_schema" add column "shopid" uuid + null; diff --git a/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/down.sql b/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/down.sql new file mode 100644 index 000000000..61b827d98 --- /dev/null +++ b/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/down.sql @@ -0,0 +1 @@ +alter table "public"."job_ar_schema" drop constraint "job_ar_schema_id_fkey"; diff --git a/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/up.sql b/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/up.sql new file mode 100644 index 000000000..238c13e33 --- /dev/null +++ b/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/up.sql @@ -0,0 +1,5 @@ +alter table "public"."job_ar_schema" + add constraint "job_ar_schema_id_fkey" + foreign key ("id") + references "public"."jobs" + ("id") on update restrict on delete restrict; diff --git a/hasura/migrations/1705952926623_run_sql_migration/down.sql b/hasura/migrations/1705952926623_run_sql_migration/down.sql new file mode 100644 index 000000000..ff68f3148 --- /dev/null +++ b/hasura/migrations/1705952926623_run_sql_migration/down.sql @@ -0,0 +1,35 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced, +-- j.shopid +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705952926623_run_sql_migration/up.sql b/hasura/migrations/1705952926623_run_sql_migration/up.sql new file mode 100644 index 000000000..2ac974bd5 --- /dev/null +++ b/hasura/migrations/1705952926623_run_sql_migration/up.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced, + j.shopid +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0; + + +END +$function$; From 7f587680cabf5defe76095e48d79a01ca82237b6 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 22 Jan 2024 16:38:26 -0500 Subject: [PATCH 06/48] - Scaffolding. Signed-off-by: Dave Richer --- server.js | 279 ++++++++++++++++++------------------ server/job/job-lifecycle.js | 5 + server/job/job.js | 1 + 3 files changed, 147 insertions(+), 138 deletions(-) create mode 100644 server/job/job-lifecycle.js diff --git a/server.js b/server.js index 5eebba671..39a85d2e1 100644 --- a/server.js +++ b/server.js @@ -12,10 +12,10 @@ const upload = multer(); //var enforce = require("express-sslify"); require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), }); const app = express(); @@ -24,25 +24,25 @@ const port = process.env.PORT || 5000; const http = require("http"); const server = http.createServer(app); -const { Server } = require("socket.io"); +const {Server} = require("socket.io"); const io = new Server(server, { - path: "/ws", - cors: { - origin: [ - "https://test.imex.online", - "https://www.test.imex.online", - "http://localhost:3000", - "https://imex.online", - "https://www.imex.online", - "https://beta.test.imex.online", - "https://www.beta.test.imex.online", - "https://beta.imex.online", - "https://www.beta.imex.online", - ], - methods: ["GET", "POST"], - credentials: true, - exposedHeaders: ["set-cookie"], - }, + path: "/ws", + cors: { + origin: [ + "https://test.imex.online", + "https://www.test.imex.online", + "http://localhost:3000", + "https://imex.online", + "https://www.imex.online", + "https://beta.test.imex.online", + "https://www.beta.test.imex.online", + "https://beta.imex.online", + "https://www.beta.imex.online", + ], + methods: ["GET", "POST"], + credentials: true, + exposedHeaders: ["set-cookie"], + }, }); exports.io = io; require("./server/web-sockets/web-socket"); @@ -50,116 +50,119 @@ require("./server/web-sockets/web-socket"); // app.use(fb.validateFirebaseIdToken); app.use(compression()); app.use(cookieParser()); -app.use(bodyParser.json({ limit: "50mb" })); -app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); +app.use(bodyParser.json({limit: "50mb"})); +app.use(bodyParser.urlencoded({limit: "50mb", extended: true})); // app.use(enforce.HTTPS({ trustProtoHeader: true })); app.use( - cors({ credentials: true, exposedHeaders: ["set-cookie"] }) - // cors({ - // credentials: true, - // origin: [ - // "https://test.imex.online", - // "http://localhost:3000", - // "https://imex.online", - // ], - // }) + cors({credentials: true, exposedHeaders: ["set-cookie"]}) + // cors({ + // credentials: true, + // origin: [ + // "https://test.imex.online", + // "http://localhost:3000", + // "https://imex.online", + // ], + // }) ); + //Email Based Paths. -var sendEmail = require("./server/email/sendemail.js"); +const sendEmail = require("./server/email/sendemail.js"); app.post("/sendemail", fb.validateFirebaseIdToken, sendEmail.sendEmail); app.post("/emailbounce", bodyParser.text(), sendEmail.emailBounce); //Test route to ensure Express is responding. -app.get("/test", async function (req, res) { - const commit = require("child_process").execSync( - "git rev-parse --short HEAD" - ); - // console.log(app.get('trust proxy')); - // console.log("remoteAddress", req.socket.remoteAddress); - // console.log("X-Forwarded-For", req.header('x-forwarded-for')); - logger.log("test-api-status", "DEBUG", "api", { commit }); - // sendEmail.sendServerEmail({ - // subject: `API Check - ${process.env.NODE_ENV}`, - // text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`, - // }); - sendEmail.sendServerEmail({ - subject: `API Check - ${process.env.NODE_ENV}`, - text: `Server API check has come in.`, - }); - res.status(200).send(`OK - ${commit}`); +app.get("/test", async (req, res) => { + const commit = require("child_process").execSync( + "git rev-parse --short HEAD" + ); + // console.log(app.get('trust proxy')); + // console.log("remoteAddress", req.socket.remoteAddress); + // console.log("X-Forwarded-For", req.header('x-forwarded-for')); + logger.log("test-api-status", "DEBUG", "api", {commit}); + // sendEmail.sendServerEmail({ + // subject: `API Check - ${process.env.NODE_ENV}`, + // text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`, + // }); + sendEmail.sendServerEmail({ + subject: `API Check - ${process.env.NODE_ENV}`, + text: `Server API check has come in.`, + }); + res.status(200).send(`OK - ${commit}`); }); //Accounting Qbxml const accountQbxml = require("./server/accounting/qbxml/qbxml"); app.post( - "/accounting/qbxml/receivables", - fb.validateFirebaseIdToken, - accountQbxml.receivables + "/accounting/qbxml/receivables", + fb.validateFirebaseIdToken, + accountQbxml.receivables ); app.post( - "/accounting/qbxml/payables", - fb.validateFirebaseIdToken, - accountQbxml.payables + "/accounting/qbxml/payables", + fb.validateFirebaseIdToken, + accountQbxml.payables ); app.post( - "/accounting/qbxml/payments", - fb.validateFirebaseIdToken, - accountQbxml.payments + "/accounting/qbxml/payments", + fb.validateFirebaseIdToken, + accountQbxml.payments ); //Cloudinary Media Paths -var media = require("./server/media/media"); +const media = require("./server/media/media"); app.post( - "/media/sign", - fb.validateFirebaseIdToken, - media.createSignedUploadURL + "/media/sign", + fb.validateFirebaseIdToken, + media.createSignedUploadURL ); app.post("/media/download", fb.validateFirebaseIdToken, media.downloadFiles); app.post("/media/rename", fb.validateFirebaseIdToken, media.renameKeys); app.post("/media/delete", fb.validateFirebaseIdToken, media.deleteFiles); //SMS/Twilio Paths -var smsReceive = require("./server/sms/receive"); +const smsReceive = require("./server/sms/receive"); app.post( - "/sms/receive", - twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }), - smsReceive.receive + "/sms/receive", + twilio.webhook({validate: process.env.NODE_ENV === "PRODUCTION"}), + smsReceive.receive ); -var smsSend = require("./server/sms/send"); +const smsSend = require("./server/sms/send"); app.post("/sms/send", fb.validateFirebaseIdToken, smsSend.send); -var smsStatus = require("./server/sms/status"); +const smsStatus = require("./server/sms/status"); app.post( - "/sms/status", - twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }), - smsStatus.status + "/sms/status", + twilio.webhook({validate: process.env.NODE_ENV === "PRODUCTION"}), + smsStatus.status ); app.post( - "/sms/markConversationRead", - fb.validateFirebaseIdToken, - smsStatus.markConversationRead + "/sms/markConversationRead", + fb.validateFirebaseIdToken, + smsStatus.markConversationRead ); -var job = require("./server/job/job"); +const job = require("./server/job/job"); app.post("/job/totals", fb.validateFirebaseIdToken, job.totals); app.post( - "/job/statustransition", - // fb.validateFirebaseIdToken, - job.statustransition + "/job/statustransition", + // fb.validateFirebaseIdToken, + job.statustransition ); app.post("/job/totalsssu", fb.validateFirebaseIdToken, job.totalsSsu); app.post("/job/costing", fb.validateFirebaseIdToken, job.costing); +app.get("/job/lifecycle", fb.validateFirebaseIdToken, job.lifecycle); + app.post("/job/costingmulti", fb.validateFirebaseIdToken, job.costingmulti); -var partsScan = require("./server/parts-scan/parts-scan"); +const partsScan = require("./server/parts-scan/parts-scan"); app.post("/job/partsscan", fb.validateFirebaseIdToken, partsScan.partsScan); //Scheduling -var scheduling = require("./server/scheduling/scheduling-job"); +const scheduling = require("./server/scheduling/scheduling-job"); app.post("/scheduling/job", fb.validateFirebaseIdToken, scheduling.job); //Handlebars Paths for Email/Report Rendering // var renderHandlebars = require("./server/render/renderHandlebars"); // app.post("/render", fb.validateFirebaseIdToken, renderHandlebars.render); -var inlineCss = require("./server/render/inlinecss"); +const inlineCss = require("./server/render/inlinecss"); app.post("/render/inlinecss", fb.validateFirebaseIdToken, inlineCss.inlinecss); // app.post( @@ -169,37 +172,37 @@ app.post("/render/inlinecss", fb.validateFirebaseIdToken, inlineCss.inlinecss); // ); app.post("/notifications/subscribe", fb.validateFirebaseIdToken, fb.subscribe); app.post( - "/notifications/unsubscribe", - fb.validateFirebaseIdToken, - fb.unsubscribe + "/notifications/unsubscribe", + fb.validateFirebaseIdToken, + fb.unsubscribe ); app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser); app.post("/adm/getuser", fb.validateFirebaseIdToken, fb.getUser); app.post("/adm/createuser", fb.validateFirebaseIdToken, fb.createUser); const adm = require("./server/admin/adminops"); app.post( - "/adm/createassociation", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.createAssociation + "/adm/createassociation", + fb.validateFirebaseIdToken, + fb.validateAdmin, + adm.createAssociation ); app.post( - "/adm/createshop", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.createShop + "/adm/createshop", + fb.validateFirebaseIdToken, + fb.validateAdmin, + adm.createShop ); app.post( - "/adm/updateshop", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.updateShop + "/adm/updateshop", + fb.validateFirebaseIdToken, + fb.validateAdmin, + adm.updateShop ); app.post( - "/adm/updatecounter", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.updateCounter + "/adm/updatecounter", + fb.validateFirebaseIdToken, + fb.validateAdmin, + adm.updateCounter ); //Stripe Processing @@ -212,88 +215,88 @@ app.post( // ); //Tech Console -var tech = require("./server/tech/tech"); +const tech = require("./server/tech/tech"); app.post("/tech/login", fb.validateFirebaseIdToken, tech.techLogin); -var utils = require("./server/utils/utils"); +const utils = require("./server/utils/utils"); app.post("/utils/time", utils.servertime); app.post("/utils/jsr", fb.validateFirebaseIdToken, utils.jsrAuth); -var qbo = require("./server/accounting/qbo/qbo"); +const qbo = require("./server/accounting/qbo/qbo"); app.post("/qbo/authorize", fb.validateFirebaseIdToken, qbo.authorize); app.get("/qbo/callback", qbo.callback); app.post("/qbo/receivables", fb.validateFirebaseIdToken, qbo.receivables); app.post("/qbo/payables", fb.validateFirebaseIdToken, qbo.payables); app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments); -var data = require("./server/data/data"); +const data = require("./server/data/data"); app.post("/data/ah", data.autohouse); app.post("/data/cc", data.claimscorp); app.post("/data/kaizen", data.kaizen); app.post("/record-handler/arms", data.arms); -var taskHandler = require("./server/tasks/tasks"); +const taskHandler = require("./server/tasks/tasks"); app.post("/taskHandler", fb.validateFirebaseIdToken, taskHandler.taskHandler); -var mixdataUpload = require("./server/mixdata/mixdata"); +const mixdataUpload = require("./server/mixdata/mixdata"); app.post( - "/mixdata/upload", - fb.validateFirebaseIdToken, - upload.any(), - mixdataUpload.mixdataUpload + "/mixdata/upload", + fb.validateFirebaseIdToken, + upload.any(), + mixdataUpload.mixdataUpload ); -var intellipay = require("./server/intellipay/intellipay"); +const intellipay = require("./server/intellipay/intellipay"); app.post( - "/intellipay/lightbox_credentials", - fb.validateFirebaseIdToken, - intellipay.lightbox_credentials + "/intellipay/lightbox_credentials", + fb.validateFirebaseIdToken, + intellipay.lightbox_credentials ); app.post( - "/intellipay/payment_refund", - fb.validateFirebaseIdToken, - intellipay.payment_refund + "/intellipay/payment_refund", + fb.validateFirebaseIdToken, + intellipay.payment_refund ); app.post( - "/intellipay/generate_payment_url", - fb.validateFirebaseIdToken, - intellipay.generate_payment_url + "/intellipay/generate_payment_url", + fb.validateFirebaseIdToken, + intellipay.generate_payment_url ); app.post( - "/intellipay/postback", - // fb.validateFirebaseIdToken, - intellipay.postback + "/intellipay/postback", + // fb.validateFirebaseIdToken, + intellipay.postback ); -var ioevent = require("./server/ioevent/ioevent"); +const ioevent = require("./server/ioevent/ioevent"); app.post("/ioevent", ioevent.default); // app.post("/newlog", (req, res) => { // const { message, type, user, record, object } = req.body; // logger.log(message, type, user, record, object); // }); -var os = require("./server/opensearch/os-handler"); +const os = require("./server/opensearch/os-handler"); app.post( - "/opensearch", //fb.validateFirebaseIdToken, - os.handler + "/opensearch", //fb.validateFirebaseIdToken, + os.handler ); app.post("/search", fb.validateFirebaseIdToken, os.search); -var cdkGetMake = require("./server/cdk/cdk-get-makes"); +const cdkGetMake = require("./server/cdk/cdk-get-makes"); app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default); -app.get("/", async function (req, res) { - res.status(200).send("Access Forbidden."); +app.get("/", async (req, res) => { + res.status(200).send("Access Forbidden."); }); server.listen(port, (error) => { - if (error) throw error; - logger.log( - `[${process.env.NODE_ENV || "DEVELOPMENT"}] Server running on port ${port}`, - "INFO", - "api" - ); + if (error) throw error; + logger.log( + `[${process.env.NODE_ENV || "DEVELOPMENT"}] Server running on port ${port}`, + "INFO", + "api" + ); }); diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js new file mode 100644 index 000000000..3834bf1cb --- /dev/null +++ b/server/job/job-lifecycle.js @@ -0,0 +1,5 @@ +const jobLifecycle = (req, res) => { + return res.status(200).send("jobLifecycle"); +}; + +module.exports = jobLifecycle; \ No newline at end of file diff --git a/server/job/job.js b/server/job/job.js index e0fff1695..f21cbf4f4 100644 --- a/server/job/job.js +++ b/server/job/job.js @@ -3,3 +3,4 @@ exports.totalsSsu = require("./job-totals").totalsSsu; exports.costing = require("./job-costing").JobCosting; exports.costingmulti = require("./job-costing").JobCostingMulti; exports.statustransition = require("./job-status-transition").statustransition; +exports.lifecycle = require('./job-lifecycle'); \ No newline at end of file From eabbc2211bac8153941c029ef09678f09851da50 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 22 Jan 2024 17:21:46 -0500 Subject: [PATCH 07/48] - Minor cleanup Signed-off-by: Dave Richer --- server.js | 326 +++++------------------- server/accounting/pbs/pbs-job-export.js | 2 +- server/routes/accountingRoutes.js | 10 + server/routes/adminRoutes.js | 15 ++ server/routes/cdkRoutes.js | 8 + server/routes/dataRoutes.js | 10 + server/routes/emailRoutes.js | 10 + server/routes/intellipayRoutes.js | 11 + server/routes/ioeventRoutes.js | 7 + server/routes/jobRoutes.js | 15 ++ server/routes/mediaRoutes.js | 11 + server/routes/miscellaneousRoutes.js | 33 +++ server/routes/mixDataRoutes.js | 10 + server/routes/osRoutes.js | 9 + server/routes/schedulingRoutes.js | 8 + server/routes/smsRoutes.js | 17 ++ server/routes/techRoutes.js | 8 + server/routes/utilRoutes.js | 9 + 18 files changed, 262 insertions(+), 257 deletions(-) create mode 100644 server/routes/accountingRoutes.js create mode 100644 server/routes/adminRoutes.js create mode 100644 server/routes/cdkRoutes.js create mode 100644 server/routes/dataRoutes.js create mode 100644 server/routes/emailRoutes.js create mode 100644 server/routes/intellipayRoutes.js create mode 100644 server/routes/ioeventRoutes.js create mode 100644 server/routes/jobRoutes.js create mode 100644 server/routes/mediaRoutes.js create mode 100644 server/routes/miscellaneousRoutes.js create mode 100644 server/routes/mixDataRoutes.js create mode 100644 server/routes/osRoutes.js create mode 100644 server/routes/schedulingRoutes.js create mode 100644 server/routes/smsRoutes.js create mode 100644 server/routes/techRoutes.js create mode 100644 server/routes/utilRoutes.js diff --git a/server.js b/server.js index 39a85d2e1..45bcb6756 100644 --- a/server.js +++ b/server.js @@ -1,30 +1,43 @@ +// Import core modules const express = require("express"); const cors = require("cors"); const bodyParser = require("body-parser"); const path = require("path"); const compression = require("compression"); -const twilio = require("twilio"); -const logger = require("./server/utils/logger"); -const fb = require("./server/firebase/firebase-handler"); const cookieParser = require("cookie-parser"); -const multer = require("multer"); -const upload = multer(); -//var enforce = require("express-sslify"); +const http = require("http"); +const {Server} = require("socket.io"); +// Load environment variables require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); +// Import custom utilities and handlers +const logger = require("./server/utils/logger"); + +const countRoutes = (app) => { + let routeCount = 0; + + app._router.stack.forEach(function(middleware){ + if(middleware.route){ // if it's a route, count it + routeCount++; + } else if(middleware.name === 'router'){ // if it's a router, count its routes + middleware.handle.stack.forEach(function(handler){ + if(handler.route){ + routeCount++; + } + }); + } + }); + + return routeCount; +} + +// Express app and server setup const app = express(); const port = process.env.PORT || 5000; -//const port = 5000; - -const http = require("http"); const server = http.createServer(app); -const {Server} = require("socket.io"); const io = new Server(server, { path: "/ws", cors: { @@ -45,258 +58,59 @@ const io = new Server(server, { }, }); exports.io = io; + require("./server/web-sockets/web-socket"); -// app.set('trust proxy', true) -// app.use(fb.validateFirebaseIdToken); + +// Import route handlers (assuming these files are structured accordingly) +const emailRoutes = require("./server/routes/emailRoutes"); +const accountingRoutes = require("./server/routes/accountingRoutes"); +const mediaRoutes = require("./server/routes/mediaRoutes"); +const smsRoutes = require("./server/routes/smsRoutes"); +const jobRoutes = require("./server/routes/jobRoutes"); +const schedulingRoutes = require("./server/routes/schedulingRoutes"); +const utilRoutes = require("./server/routes/utilRoutes"); +const dataRoutes = require("./server/routes/dataRoutes"); +const adminRoutes = require("./server/routes/adminRoutes"); +const techRoutes = require("./server/routes/techRoutes"); +const intellipayRoutes = require("./server/routes/intellipayRoutes"); +const ioeventRoutes = require("./server/routes/ioeventRoutes"); +const osRoutes = require("./server/routes/osRoutes"); +const cdkRoutes = require("./server/routes/cdkRoutes"); +const miscellaneousRoutes = require("./server/routes/miscellaneousRoutes"); +const mixdataRoutes = require("./server/routes/mixDataRoutes"); + +// Middleware app.use(compression()); app.use(cookieParser()); app.use(bodyParser.json({limit: "50mb"})); app.use(bodyParser.urlencoded({limit: "50mb", extended: true})); -// app.use(enforce.HTTPS({ trustProtoHeader: true })); -app.use( - cors({credentials: true, exposedHeaders: ["set-cookie"]}) - // cors({ - // credentials: true, - // origin: [ - // "https://test.imex.online", - // "http://localhost:3000", - // "https://imex.online", - // ], - // }) -); +app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]})); +// Route groupings +app.use('/mixdata', mixdataRoutes); +app.use('/email', emailRoutes); +app.use('/accounting', accountingRoutes); +app.use('/media', mediaRoutes); +app.use('/sms', smsRoutes); +app.use('/job', jobRoutes); +app.use('/scheduling', schedulingRoutes); +app.use('/utils', utilRoutes); +app.use('/data', dataRoutes); +app.use('/admin', adminRoutes); +app.use('/tech', techRoutes); +app.use('/intellipay', intellipayRoutes); +app.use('/ioevent', ioeventRoutes); +app.use('/opensearch', osRoutes); +app.use('/cdk', cdkRoutes); +app.use('/', miscellaneousRoutes); -//Email Based Paths. -const sendEmail = require("./server/email/sendemail.js"); -app.post("/sendemail", fb.validateFirebaseIdToken, sendEmail.sendEmail); -app.post("/emailbounce", bodyParser.text(), sendEmail.emailBounce); - -//Test route to ensure Express is responding. -app.get("/test", async (req, res) => { - const commit = require("child_process").execSync( - "git rev-parse --short HEAD" - ); - // console.log(app.get('trust proxy')); - // console.log("remoteAddress", req.socket.remoteAddress); - // console.log("X-Forwarded-For", req.header('x-forwarded-for')); - logger.log("test-api-status", "DEBUG", "api", {commit}); - // sendEmail.sendServerEmail({ - // subject: `API Check - ${process.env.NODE_ENV}`, - // text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`, - // }); - sendEmail.sendServerEmail({ - subject: `API Check - ${process.env.NODE_ENV}`, - text: `Server API check has come in.`, - }); - res.status(200).send(`OK - ${commit}`); -}); - -//Accounting Qbxml -const accountQbxml = require("./server/accounting/qbxml/qbxml"); -app.post( - "/accounting/qbxml/receivables", - fb.validateFirebaseIdToken, - accountQbxml.receivables -); -app.post( - "/accounting/qbxml/payables", - fb.validateFirebaseIdToken, - accountQbxml.payables -); -app.post( - "/accounting/qbxml/payments", - fb.validateFirebaseIdToken, - accountQbxml.payments -); - -//Cloudinary Media Paths -const media = require("./server/media/media"); -app.post( - "/media/sign", - fb.validateFirebaseIdToken, - media.createSignedUploadURL -); -app.post("/media/download", fb.validateFirebaseIdToken, media.downloadFiles); -app.post("/media/rename", fb.validateFirebaseIdToken, media.renameKeys); -app.post("/media/delete", fb.validateFirebaseIdToken, media.deleteFiles); - -//SMS/Twilio Paths -const smsReceive = require("./server/sms/receive"); -app.post( - "/sms/receive", - twilio.webhook({validate: process.env.NODE_ENV === "PRODUCTION"}), - smsReceive.receive -); -const smsSend = require("./server/sms/send"); -app.post("/sms/send", fb.validateFirebaseIdToken, smsSend.send); -const smsStatus = require("./server/sms/status"); -app.post( - "/sms/status", - twilio.webhook({validate: process.env.NODE_ENV === "PRODUCTION"}), - smsStatus.status -); -app.post( - "/sms/markConversationRead", - fb.validateFirebaseIdToken, - smsStatus.markConversationRead -); - -const job = require("./server/job/job"); -app.post("/job/totals", fb.validateFirebaseIdToken, job.totals); -app.post( - "/job/statustransition", - // fb.validateFirebaseIdToken, - job.statustransition -); -app.post("/job/totalsssu", fb.validateFirebaseIdToken, job.totalsSsu); -app.post("/job/costing", fb.validateFirebaseIdToken, job.costing); -app.get("/job/lifecycle", fb.validateFirebaseIdToken, job.lifecycle); - -app.post("/job/costingmulti", fb.validateFirebaseIdToken, job.costingmulti); -const partsScan = require("./server/parts-scan/parts-scan"); -app.post("/job/partsscan", fb.validateFirebaseIdToken, partsScan.partsScan); -//Scheduling -const scheduling = require("./server/scheduling/scheduling-job"); -app.post("/scheduling/job", fb.validateFirebaseIdToken, scheduling.job); - -//Handlebars Paths for Email/Report Rendering -// var renderHandlebars = require("./server/render/renderHandlebars"); -// app.post("/render", fb.validateFirebaseIdToken, renderHandlebars.render); -const inlineCss = require("./server/render/inlinecss"); -app.post("/render/inlinecss", fb.validateFirebaseIdToken, inlineCss.inlinecss); - -// app.post( -// "/notifications/send", - -// fb.sendNotification -// ); -app.post("/notifications/subscribe", fb.validateFirebaseIdToken, fb.subscribe); -app.post( - "/notifications/unsubscribe", - fb.validateFirebaseIdToken, - fb.unsubscribe -); -app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser); -app.post("/adm/getuser", fb.validateFirebaseIdToken, fb.getUser); -app.post("/adm/createuser", fb.validateFirebaseIdToken, fb.createUser); -const adm = require("./server/admin/adminops"); -app.post( - "/adm/createassociation", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.createAssociation -); -app.post( - "/adm/createshop", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.createShop -); -app.post( - "/adm/updateshop", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.updateShop -); -app.post( - "/adm/updatecounter", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.updateCounter -); - -//Stripe Processing -// var stripe = require("./server/stripe/payment"); -// app.post("/stripe/payment", fb.validateFirebaseIdToken, stripe.payment); -// app.post( -// "/stripe/mobilepayment", -// fb.validateFirebaseIdToken, -// stripe.mobile_payment -// ); - -//Tech Console -const tech = require("./server/tech/tech"); -app.post("/tech/login", fb.validateFirebaseIdToken, tech.techLogin); - -const utils = require("./server/utils/utils"); -app.post("/utils/time", utils.servertime); -app.post("/utils/jsr", fb.validateFirebaseIdToken, utils.jsrAuth); -const qbo = require("./server/accounting/qbo/qbo"); -app.post("/qbo/authorize", fb.validateFirebaseIdToken, qbo.authorize); -app.get("/qbo/callback", qbo.callback); -app.post("/qbo/receivables", fb.validateFirebaseIdToken, qbo.receivables); -app.post("/qbo/payables", fb.validateFirebaseIdToken, qbo.payables); -app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments); - -const data = require("./server/data/data"); -app.post("/data/ah", data.autohouse); -app.post("/data/cc", data.claimscorp); -app.post("/data/kaizen", data.kaizen); -app.post("/record-handler/arms", data.arms); - -const taskHandler = require("./server/tasks/tasks"); -app.post("/taskHandler", fb.validateFirebaseIdToken, taskHandler.taskHandler); - -const mixdataUpload = require("./server/mixdata/mixdata"); - -app.post( - "/mixdata/upload", - fb.validateFirebaseIdToken, - upload.any(), - mixdataUpload.mixdataUpload -); - -const intellipay = require("./server/intellipay/intellipay"); -app.post( - "/intellipay/lightbox_credentials", - fb.validateFirebaseIdToken, - intellipay.lightbox_credentials -); - -app.post( - "/intellipay/payment_refund", - fb.validateFirebaseIdToken, - intellipay.payment_refund -); - -app.post( - "/intellipay/generate_payment_url", - fb.validateFirebaseIdToken, - intellipay.generate_payment_url -); - -app.post( - "/intellipay/postback", - // fb.validateFirebaseIdToken, - intellipay.postback -); - -const ioevent = require("./server/ioevent/ioevent"); -app.post("/ioevent", ioevent.default); -// app.post("/newlog", (req, res) => { -// const { message, type, user, record, object } = req.body; -// logger.log(message, type, user, record, object); -// }); - -const os = require("./server/opensearch/os-handler"); -app.post( - "/opensearch", //fb.validateFirebaseIdToken, - os.handler -); -app.post("/search", fb.validateFirebaseIdToken, os.search); - -const cdkGetMake = require("./server/cdk/cdk-get-makes"); -app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default); - -app.get("/", async (req, res) => { +// Default route for forbidden access +app.get("/", (req, res) => { res.status(200).send("Access Forbidden."); }); +// Start server server.listen(port, (error) => { if (error) throw error; - logger.log( - `[${process.env.NODE_ENV || "DEVELOPMENT"}] Server running on port ${port}`, - "INFO", - "api" - ); + logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server with ${countRoutes(app)} routes running on port ${port}`, "INFO", "api"); }); diff --git a/server/accounting/pbs/pbs-job-export.js b/server/accounting/pbs/pbs-job-export.js index dd63a975d..08fe9ee14 100644 --- a/server/accounting/pbs/pbs-job-export.js +++ b/server/accounting/pbs/pbs-job-export.js @@ -166,7 +166,7 @@ async function CheckForErrors(socket, response) { CdkBase.createLogEvent( socket, "DEBUG", - `Succesful response from DMS. ${response.Message || ""}` + `Successful response from DMS. ${response.Message || ""}` ); } else { CdkBase.createLogEvent( diff --git a/server/routes/accountingRoutes.js b/server/routes/accountingRoutes.js new file mode 100644 index 000000000..9e390746d --- /dev/null +++ b/server/routes/accountingRoutes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const accountQbxml = require('../accounting/qbxml/qbxml'); + +router.post('/qbxml/receivables', fb.validateFirebaseIdToken, accountQbxml.receivables); +router.post('/qbxml/payables', fb.validateFirebaseIdToken, accountQbxml.payables); +router.post('/qbxml/payments', fb.validateFirebaseIdToken, accountQbxml.payments); + +module.exports = router; diff --git a/server/routes/adminRoutes.js b/server/routes/adminRoutes.js new file mode 100644 index 000000000..9fffd1568 --- /dev/null +++ b/server/routes/adminRoutes.js @@ -0,0 +1,15 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const adm = require('../admin/adminops'); + + +router.post('/createassociation', fb.validateFirebaseIdToken, fb.validateAdmin, adm.createAssociation); +router.post('/createshop', fb.validateFirebaseIdToken, fb.validateAdmin, adm.createShop); +router.post('/updateshop', fb.validateFirebaseIdToken, fb.validateAdmin, adm.updateShop); +router.post('/updatecounter', fb.validateFirebaseIdToken, fb.validateAdmin, adm.updateCounter); +router.post('/updateuser', fb.validateFirebaseIdToken, fb.updateUser); +router.post('/getuser', fb.validateFirebaseIdToken, fb.getUser); +router.post('/createuser', fb.validateFirebaseIdToken, fb.createUser); + +module.exports = router; diff --git a/server/routes/cdkRoutes.js b/server/routes/cdkRoutes.js new file mode 100644 index 000000000..9aa234655 --- /dev/null +++ b/server/routes/cdkRoutes.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const cdkGetMake = require('../cdk/cdk-get-makes'); + +router.post('/getvehicles', fb.validateFirebaseIdToken, cdkGetMake.default); + +module.exports = router; diff --git a/server/routes/dataRoutes.js b/server/routes/dataRoutes.js new file mode 100644 index 000000000..d4db15523 --- /dev/null +++ b/server/routes/dataRoutes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const data = require('../data/data'); + +router.post('/ah', data.autohouse); +router.post('/cc', data.claimscorp); +router.post('/kaizen', data.kaizen); +router.post('/arms', data.arms); + +module.exports = router; diff --git a/server/routes/emailRoutes.js b/server/routes/emailRoutes.js new file mode 100644 index 000000000..51f79079e --- /dev/null +++ b/server/routes/emailRoutes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const bodyParser = require('body-parser'); +const sendEmail = require('../email/sendemail'); +const fb = require('../firebase/firebase-handler'); + +router.post('/sendemail', fb.validateFirebaseIdToken, sendEmail.sendEmail); +router.post('/emailbounce', bodyParser.text(), sendEmail.emailBounce); + +module.exports = router; \ No newline at end of file diff --git a/server/routes/intellipayRoutes.js b/server/routes/intellipayRoutes.js new file mode 100644 index 000000000..88144f1be --- /dev/null +++ b/server/routes/intellipayRoutes.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const intellipay = require('../intellipay/intellipay'); + +router.post('/lightbox_credentials', fb.validateFirebaseIdToken, intellipay.lightbox_credentials); +router.post('/payment_refund', fb.validateFirebaseIdToken, intellipay.payment_refund); +router.post('/generate_payment_url', fb.validateFirebaseIdToken, intellipay.generate_payment_url); +router.post('/postback', intellipay.postback); + +module.exports = router; diff --git a/server/routes/ioeventRoutes.js b/server/routes/ioeventRoutes.js new file mode 100644 index 000000000..be8ca2ab9 --- /dev/null +++ b/server/routes/ioeventRoutes.js @@ -0,0 +1,7 @@ +const express = require('express'); +const router = express.Router(); +const ioevent = require('../ioevent/ioevent'); + +router.post('/', ioevent.default); + +module.exports = router; diff --git a/server/routes/jobRoutes.js b/server/routes/jobRoutes.js new file mode 100644 index 000000000..f3cc7bf1b --- /dev/null +++ b/server/routes/jobRoutes.js @@ -0,0 +1,15 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const job = require('../job/job'); +const partsScan = require('../parts-scan/parts-scan'); + +router.post('/totals', fb.validateFirebaseIdToken, job.totals); +router.post('/statustransition', fb.validateFirebaseIdToken, job.statustransition); +router.post('/totalsssu', fb.validateFirebaseIdToken, job.totalsSsu); +router.post('/costing', fb.validateFirebaseIdToken, job.costing); +router.get('/lifecycle', fb.validateFirebaseIdToken, job.lifecycle); +router.post('/costingmulti', fb.validateFirebaseIdToken, job.costingmulti); +router.post('/partsscan', fb.validateFirebaseIdToken, partsScan.partsScan); + +module.exports = router; diff --git a/server/routes/mediaRoutes.js b/server/routes/mediaRoutes.js new file mode 100644 index 000000000..3d6d9f69e --- /dev/null +++ b/server/routes/mediaRoutes.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const media = require('../media/media'); + +router.post('/sign', fb.validateFirebaseIdToken, media.createSignedUploadURL); +router.post('/download', fb.validateFirebaseIdToken, media.downloadFiles); +router.post('/rename', fb.validateFirebaseIdToken, media.renameKeys); +router.post('/delete', fb.validateFirebaseIdToken, media.deleteFiles); + +module.exports = router; diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js new file mode 100644 index 000000000..bd8e9e961 --- /dev/null +++ b/server/routes/miscellaneousRoutes.js @@ -0,0 +1,33 @@ +const express = require('express'); +const router = express.Router(); +const logger = require("../../server/utils/logger"); +const sendEmail = require("../email/sendemail"); + +// Import any necessary handlers or utilities + +// Define miscellaneous routes here +// Example: +// router.get('/some-route', someHandler); + +//Test route to ensure Express is responding. +router.get("/test", async function (req, res) { + const commit = require("child_process").execSync( + "git rev-parse --short HEAD" + ); + // console.log(app.get('trust proxy')); + // console.log("remoteAddress", req.socket.remoteAddress); + // console.log("X-Forwarded-For", req.header('x-forwarded-for')); + logger.log("test-api-status", "DEBUG", "api", { commit }); + // sendEmail.sendServerEmail({ + // subject: `API Check - ${process.env.NODE_ENV}`, + // text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`, + // }); + sendEmail.sendServerEmail({ + subject: `API Check - ${process.env.NODE_ENV}`, + text: `Server API check has come in.`, + }); + res.status(200).send(`OK - ${commit}`); +}); + + +module.exports = router; diff --git a/server/routes/mixDataRoutes.js b/server/routes/mixDataRoutes.js new file mode 100644 index 000000000..67559bd9a --- /dev/null +++ b/server/routes/mixDataRoutes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const multer = require('multer'); +const upload = multer(); +const fb = require('../firebase/firebase-handler'); +const mixdataUpload = require('../mixdata/mixdata'); + +router.post('/upload', fb.validateFirebaseIdToken, upload.any(), mixdataUpload.mixdataUpload); + +module.exports = router; diff --git a/server/routes/osRoutes.js b/server/routes/osRoutes.js new file mode 100644 index 000000000..04b93300c --- /dev/null +++ b/server/routes/osRoutes.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const osHandler = require('../opensearch/os-handler'); + +router.post('/', osHandler.handler); +router.post('/search', fb.validateFirebaseIdToken, osHandler.search); + +module.exports = router; diff --git a/server/routes/schedulingRoutes.js b/server/routes/schedulingRoutes.js new file mode 100644 index 000000000..36a934102 --- /dev/null +++ b/server/routes/schedulingRoutes.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const scheduling = require('../scheduling/scheduling-job'); + +router.post('/job', fb.validateFirebaseIdToken, scheduling.job); + +module.exports = router; diff --git a/server/routes/smsRoutes.js b/server/routes/smsRoutes.js new file mode 100644 index 000000000..a642c61bf --- /dev/null +++ b/server/routes/smsRoutes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const router = express.Router(); +const twilio = require('twilio'); +const fb = require('../firebase/firebase-handler'); +const smsReceive = require('../sms/receive'); +const smsSend = require('../sms/send'); +const smsStatus = require('../sms/status'); + +// Twilio Webhook Middleware for production +const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }); + +router.post('/receive', twilioWebhookMiddleware, smsReceive.receive); +router.post('/send', fb.validateFirebaseIdToken, smsSend.send); +router.post('/status', twilioWebhookMiddleware, smsStatus.status); +router.post('/markConversationRead', fb.validateFirebaseIdToken, smsStatus.markConversationRead); + +module.exports = router; diff --git a/server/routes/techRoutes.js b/server/routes/techRoutes.js new file mode 100644 index 000000000..ca6d4c936 --- /dev/null +++ b/server/routes/techRoutes.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const tech = require('../tech/tech'); + +router.post('/login', fb.validateFirebaseIdToken, tech.techLogin); + +module.exports = router; diff --git a/server/routes/utilRoutes.js b/server/routes/utilRoutes.js new file mode 100644 index 000000000..173637479 --- /dev/null +++ b/server/routes/utilRoutes.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const utils = require('../utils/utils'); + +router.post('/time', utils.servertime); +router.post('/jsr', fb.validateFirebaseIdToken, utils.jsrAuth); + +module.exports = router; From 343179d4fec1eae84aed76b218f5878a3d9ce20b Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 22 Jan 2024 18:05:35 -0500 Subject: [PATCH 08/48] - Minor cleanup Signed-off-by: Dave Richer --- server.js | 14 +++++++++++--- server/routes/notificationsRoutes.js | 8 ++++++++ server/routes/renderRoutes.js | 9 +++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 server/routes/notificationsRoutes.js create mode 100644 server/routes/renderRoutes.js diff --git a/server.js b/server.js index 45bcb6756..99ea8945c 100644 --- a/server.js +++ b/server.js @@ -78,6 +78,8 @@ const osRoutes = require("./server/routes/osRoutes"); const cdkRoutes = require("./server/routes/cdkRoutes"); const miscellaneousRoutes = require("./server/routes/miscellaneousRoutes"); const mixdataRoutes = require("./server/routes/mixDataRoutes"); +const renderRoutes = require("./server/routes/renderRoutes"); +const notificationsRoutes = require("./server/routes/notificationsRoutes"); // Middleware app.use(compression()); @@ -87,8 +89,14 @@ app.use(bodyParser.urlencoded({limit: "50mb", extended: true})); app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]})); // Route groupings +app.use('/', miscellaneousRoutes); + +// Exclude name, email routes go from root. +app.use(emailRoutes); + +app.use("/notifications", notificationsRoutes); +app.use("/render", renderRoutes); app.use('/mixdata', mixdataRoutes); -app.use('/email', emailRoutes); app.use('/accounting', accountingRoutes); app.use('/media', mediaRoutes); app.use('/sms', smsRoutes); @@ -96,19 +104,19 @@ app.use('/job', jobRoutes); app.use('/scheduling', schedulingRoutes); app.use('/utils', utilRoutes); app.use('/data', dataRoutes); -app.use('/admin', adminRoutes); +app.use('/adm', adminRoutes); app.use('/tech', techRoutes); app.use('/intellipay', intellipayRoutes); app.use('/ioevent', ioeventRoutes); app.use('/opensearch', osRoutes); app.use('/cdk', cdkRoutes); -app.use('/', miscellaneousRoutes); // Default route for forbidden access app.get("/", (req, res) => { res.status(200).send("Access Forbidden."); }); + // Start server server.listen(port, (error) => { if (error) throw error; diff --git a/server/routes/notificationsRoutes.js b/server/routes/notificationsRoutes.js new file mode 100644 index 000000000..b1c5ac48e --- /dev/null +++ b/server/routes/notificationsRoutes.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); + +router.post('/subscribe', fb.validateFirebaseIdToken, fb.subscribe); +router.post('/unsubscribe', fb.validateFirebaseIdToken, fb.unsubscribe); + +module.exports = router; diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js new file mode 100644 index 000000000..87dd75e70 --- /dev/null +++ b/server/routes/renderRoutes.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const inlineCss = require('../render/inlinecss'); + +// Define the route for inline CSS rendering +router.post('/inlinecss', fb.validateFirebaseIdToken, inlineCss.inlinecss); + +module.exports = router; From ff1ceb20cb1301583a16e3cd8f521faf2bdca8c1 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 22 Jan 2024 18:39:27 -0500 Subject: [PATCH 09/48] - Minor cleanup Signed-off-by: Dave Richer --- server.js | 9 ++------- server/routes/dataRoutes.js | 1 - server/routes/emailRoutes.js | 10 ---------- server/routes/ioeventRoutes.js | 7 ------- server/routes/miscellaneousRoutes.js | 16 +++++++++++----- server/routes/payrollRoutes.js | 0 server/routes/qboRoutes.js | 13 +++++++++++++ 7 files changed, 26 insertions(+), 30 deletions(-) delete mode 100644 server/routes/emailRoutes.js delete mode 100644 server/routes/ioeventRoutes.js create mode 100644 server/routes/payrollRoutes.js create mode 100644 server/routes/qboRoutes.js diff --git a/server.js b/server.js index 99ea8945c..f45f14a91 100644 --- a/server.js +++ b/server.js @@ -62,7 +62,6 @@ exports.io = io; require("./server/web-sockets/web-socket"); // Import route handlers (assuming these files are structured accordingly) -const emailRoutes = require("./server/routes/emailRoutes"); const accountingRoutes = require("./server/routes/accountingRoutes"); const mediaRoutes = require("./server/routes/mediaRoutes"); const smsRoutes = require("./server/routes/smsRoutes"); @@ -73,13 +72,13 @@ const dataRoutes = require("./server/routes/dataRoutes"); const adminRoutes = require("./server/routes/adminRoutes"); const techRoutes = require("./server/routes/techRoutes"); const intellipayRoutes = require("./server/routes/intellipayRoutes"); -const ioeventRoutes = require("./server/routes/ioeventRoutes"); const osRoutes = require("./server/routes/osRoutes"); const cdkRoutes = require("./server/routes/cdkRoutes"); const miscellaneousRoutes = require("./server/routes/miscellaneousRoutes"); const mixdataRoutes = require("./server/routes/mixDataRoutes"); const renderRoutes = require("./server/routes/renderRoutes"); const notificationsRoutes = require("./server/routes/notificationsRoutes"); +const qboRoutes = require("./server/routes/qboRoutes"); // Middleware app.use(compression()); @@ -90,14 +89,11 @@ app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]})); // Route groupings app.use('/', miscellaneousRoutes); - -// Exclude name, email routes go from root. -app.use(emailRoutes); - app.use("/notifications", notificationsRoutes); app.use("/render", renderRoutes); app.use('/mixdata', mixdataRoutes); app.use('/accounting', accountingRoutes); +app.use('/qbo', qboRoutes); app.use('/media', mediaRoutes); app.use('/sms', smsRoutes); app.use('/job', jobRoutes); @@ -107,7 +103,6 @@ app.use('/data', dataRoutes); app.use('/adm', adminRoutes); app.use('/tech', techRoutes); app.use('/intellipay', intellipayRoutes); -app.use('/ioevent', ioeventRoutes); app.use('/opensearch', osRoutes); app.use('/cdk', cdkRoutes); diff --git a/server/routes/dataRoutes.js b/server/routes/dataRoutes.js index d4db15523..7a6c631ff 100644 --- a/server/routes/dataRoutes.js +++ b/server/routes/dataRoutes.js @@ -5,6 +5,5 @@ const data = require('../data/data'); router.post('/ah', data.autohouse); router.post('/cc', data.claimscorp); router.post('/kaizen', data.kaizen); -router.post('/arms', data.arms); module.exports = router; diff --git a/server/routes/emailRoutes.js b/server/routes/emailRoutes.js deleted file mode 100644 index 51f79079e..000000000 --- a/server/routes/emailRoutes.js +++ /dev/null @@ -1,10 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const bodyParser = require('body-parser'); -const sendEmail = require('../email/sendemail'); -const fb = require('../firebase/firebase-handler'); - -router.post('/sendemail', fb.validateFirebaseIdToken, sendEmail.sendEmail); -router.post('/emailbounce', bodyParser.text(), sendEmail.emailBounce); - -module.exports = router; \ No newline at end of file diff --git a/server/routes/ioeventRoutes.js b/server/routes/ioeventRoutes.js deleted file mode 100644 index be8ca2ab9..000000000 --- a/server/routes/ioeventRoutes.js +++ /dev/null @@ -1,7 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const ioevent = require('../ioevent/ioevent'); - -router.post('/', ioevent.default); - -module.exports = router; diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js index bd8e9e961..6c2170739 100644 --- a/server/routes/miscellaneousRoutes.js +++ b/server/routes/miscellaneousRoutes.js @@ -2,12 +2,12 @@ const express = require('express'); const router = express.Router(); const logger = require("../../server/utils/logger"); const sendEmail = require("../email/sendemail"); +const data = require("../data/data"); +const fb = require("../firebase/firebase-handler"); +const bodyParser = require("body-parser"); +const ioevent = require("../ioevent/ioevent"); +const taskHandler = require("../tasks/tasks"); -// Import any necessary handlers or utilities - -// Define miscellaneous routes here -// Example: -// router.get('/some-route', someHandler); //Test route to ensure Express is responding. router.get("/test", async function (req, res) { @@ -29,5 +29,11 @@ router.get("/test", async function (req, res) { res.status(200).send(`OK - ${commit}`); }); +router.post('/ioevent', ioevent.default); +router.post('/sendemail', fb.validateFirebaseIdToken, sendEmail.sendEmail); +router.post('/emailbounce', bodyParser.text(), sendEmail.emailBounce); +router.post('/record-handler/arms', data.arms); +router.post("/taskHandler", fb.validateFirebaseIdToken, taskHandler.taskHandler); + module.exports = router; diff --git a/server/routes/payrollRoutes.js b/server/routes/payrollRoutes.js new file mode 100644 index 000000000..e69de29bb diff --git a/server/routes/qboRoutes.js b/server/routes/qboRoutes.js new file mode 100644 index 000000000..69cb3edb9 --- /dev/null +++ b/server/routes/qboRoutes.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const qbo = require('../accounting/qbo/qbo'); // Assuming you have a qbo module for handling QuickBooks Online related functionalities + +// Define the routes for QuickBooks Online +router.post('/authorize', fb.validateFirebaseIdToken, qbo.authorize); +router.get('/callback', qbo.callback); +router.post('/receivables', fb.validateFirebaseIdToken, qbo.receivables); +router.post('/payables', fb.validateFirebaseIdToken, qbo.payables); +router.post('/payments', fb.validateFirebaseIdToken, qbo.payments); + +module.exports = router; From 272a3f579a9efa19ecba1427a706f0e01b6a367c Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 22 Jan 2024 18:53:57 -0500 Subject: [PATCH 10/48] - Minor cleanup Signed-off-by: Dave Richer --- server.js | 23 +---------------------- server/routes/miscellaneousRoutes.js | 6 +++++- server/routes/osRoutes.js | 9 --------- server/routes/payrollRoutes.js | 0 4 files changed, 6 insertions(+), 32 deletions(-) delete mode 100644 server/routes/osRoutes.js delete mode 100644 server/routes/payrollRoutes.js diff --git a/server.js b/server.js index f45f14a91..5533a7ad7 100644 --- a/server.js +++ b/server.js @@ -16,24 +16,6 @@ require("dotenv").config({ // Import custom utilities and handlers const logger = require("./server/utils/logger"); -const countRoutes = (app) => { - let routeCount = 0; - - app._router.stack.forEach(function(middleware){ - if(middleware.route){ // if it's a route, count it - routeCount++; - } else if(middleware.name === 'router'){ // if it's a router, count its routes - middleware.handle.stack.forEach(function(handler){ - if(handler.route){ - routeCount++; - } - }); - } - }); - - return routeCount; -} - // Express app and server setup const app = express(); const port = process.env.PORT || 5000; @@ -72,7 +54,6 @@ const dataRoutes = require("./server/routes/dataRoutes"); const adminRoutes = require("./server/routes/adminRoutes"); const techRoutes = require("./server/routes/techRoutes"); const intellipayRoutes = require("./server/routes/intellipayRoutes"); -const osRoutes = require("./server/routes/osRoutes"); const cdkRoutes = require("./server/routes/cdkRoutes"); const miscellaneousRoutes = require("./server/routes/miscellaneousRoutes"); const mixdataRoutes = require("./server/routes/mixDataRoutes"); @@ -103,7 +84,6 @@ app.use('/data', dataRoutes); app.use('/adm', adminRoutes); app.use('/tech', techRoutes); app.use('/intellipay', intellipayRoutes); -app.use('/opensearch', osRoutes); app.use('/cdk', cdkRoutes); // Default route for forbidden access @@ -111,9 +91,8 @@ app.get("/", (req, res) => { res.status(200).send("Access Forbidden."); }); - // Start server server.listen(port, (error) => { if (error) throw error; - logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server with ${countRoutes(app)} routes running on port ${port}`, "INFO", "api"); + logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server with routes running on port ${port}`, "INFO", "api"); }); diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js index 6c2170739..bae1a255c 100644 --- a/server/routes/miscellaneousRoutes.js +++ b/server/routes/miscellaneousRoutes.js @@ -7,7 +7,7 @@ const fb = require("../firebase/firebase-handler"); const bodyParser = require("body-parser"); const ioevent = require("../ioevent/ioevent"); const taskHandler = require("../tasks/tasks"); - +const os = require("../opensearch/os-handler"); //Test route to ensure Express is responding. router.get("/test", async function (req, res) { @@ -29,6 +29,10 @@ router.get("/test", async function (req, res) { res.status(200).send(`OK - ${commit}`); }); +router.post("/search", fb.validateFirebaseIdToken, os.search); +router.post("/opensearch", os.handler); + + router.post('/ioevent', ioevent.default); router.post('/sendemail', fb.validateFirebaseIdToken, sendEmail.sendEmail); router.post('/emailbounce', bodyParser.text(), sendEmail.emailBounce); diff --git a/server/routes/osRoutes.js b/server/routes/osRoutes.js deleted file mode 100644 index 04b93300c..000000000 --- a/server/routes/osRoutes.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -const osHandler = require('../opensearch/os-handler'); - -router.post('/', osHandler.handler); -router.post('/search', fb.validateFirebaseIdToken, osHandler.search); - -module.exports = router; diff --git a/server/routes/payrollRoutes.js b/server/routes/payrollRoutes.js deleted file mode 100644 index e69de29bb..000000000 From 82dc9e1c56e6d2c6bc7ddde053b74b3827550139 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 22 Jan 2024 19:07:16 -0500 Subject: [PATCH 11/48] - Finish cleanup Signed-off-by: Dave Richer --- server.js | 64 +++++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/server.js b/server.js index 5533a7ad7..66e665e79 100644 --- a/server.js +++ b/server.js @@ -43,23 +43,6 @@ exports.io = io; require("./server/web-sockets/web-socket"); -// Import route handlers (assuming these files are structured accordingly) -const accountingRoutes = require("./server/routes/accountingRoutes"); -const mediaRoutes = require("./server/routes/mediaRoutes"); -const smsRoutes = require("./server/routes/smsRoutes"); -const jobRoutes = require("./server/routes/jobRoutes"); -const schedulingRoutes = require("./server/routes/schedulingRoutes"); -const utilRoutes = require("./server/routes/utilRoutes"); -const dataRoutes = require("./server/routes/dataRoutes"); -const adminRoutes = require("./server/routes/adminRoutes"); -const techRoutes = require("./server/routes/techRoutes"); -const intellipayRoutes = require("./server/routes/intellipayRoutes"); -const cdkRoutes = require("./server/routes/cdkRoutes"); -const miscellaneousRoutes = require("./server/routes/miscellaneousRoutes"); -const mixdataRoutes = require("./server/routes/mixDataRoutes"); -const renderRoutes = require("./server/routes/renderRoutes"); -const notificationsRoutes = require("./server/routes/notificationsRoutes"); -const qboRoutes = require("./server/routes/qboRoutes"); // Middleware app.use(compression()); @@ -69,30 +52,37 @@ app.use(bodyParser.urlencoded({limit: "50mb", extended: true})); app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]})); // Route groupings -app.use('/', miscellaneousRoutes); -app.use("/notifications", notificationsRoutes); -app.use("/render", renderRoutes); -app.use('/mixdata', mixdataRoutes); -app.use('/accounting', accountingRoutes); -app.use('/qbo', qboRoutes); -app.use('/media', mediaRoutes); -app.use('/sms', smsRoutes); -app.use('/job', jobRoutes); -app.use('/scheduling', schedulingRoutes); -app.use('/utils', utilRoutes); -app.use('/data', dataRoutes); -app.use('/adm', adminRoutes); -app.use('/tech', techRoutes); -app.use('/intellipay', intellipayRoutes); -app.use('/cdk', cdkRoutes); +app.use('/', require("./server/routes/miscellaneousRoutes")); +app.use("/notifications", require("./server/routes/notificationsRoutes")); +app.use("/render", require("./server/routes/renderRoutes")); +app.use('/mixdata', require("./server/routes/mixDataRoutes")); +app.use('/accounting', require("./server/routes/accountingRoutes")); +app.use('/qbo', require("./server/routes/qboRoutes")); +app.use('/media', require("./server/routes/mediaRoutes")); +app.use('/sms', require("./server/routes/smsRoutes")); +app.use('/job', require("./server/routes/jobRoutes")); +app.use('/scheduling', require("./server/routes/schedulingRoutes")); +app.use('/utils', require("./server/routes/utilRoutes")); +app.use('/data', require("./server/routes/dataRoutes")); +app.use('/adm', require("./server/routes/adminRoutes")); +app.use('/tech', require("./server/routes/techRoutes")); +app.use('/intellipay', require("./server/routes/intellipayRoutes")); +app.use('/cdk', require("./server/routes/cdkRoutes")); // Default route for forbidden access app.get("/", (req, res) => { res.status(200).send("Access Forbidden."); }); +const main = async () => { + await server.listen(port); +} + // Start server -server.listen(port, (error) => { - if (error) throw error; - logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server with routes running on port ${port}`, "INFO", "api"); -}); +main() + .then(() => { + logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server started on port ${port}`, "INFO", "api"); + }) + .catch((error) => { + logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`, "ERROR", "api", error); +}); \ No newline at end of file From 2e7232bb65f48c36d48cb4998039683faefd11be Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 22 Jan 2024 23:00:31 -0500 Subject: [PATCH 12/48] - Finish cleanup Signed-off-by: Dave Richer --- server.js | 8 +- server/email/sendemail.js | 444 +++++++++--------- server/firebase/firebase-handler.js | 402 +++++++--------- server/job/job-costing.js | 1 + server/job/job-lifecycle.js | 14 +- server/job/job-status-transition.js | 129 +++-- .../eventAuthorizationMIddleware.js | 15 + server/middleware/validateAdminMiddleware.js | 15 + .../validateFirebaseIdTokenMiddleware.js | 59 +++ server/opensearch/os-handler.js | 4 - server/routes/accountingRoutes.js | 12 +- server/routes/adminRoutes.js | 19 +- server/routes/cdkRoutes.js | 6 +- server/routes/dataRoutes.js | 8 +- server/routes/intellipayRoutes.js | 12 +- server/routes/jobRoutes.js | 22 +- server/routes/mediaRoutes.js | 14 +- server/routes/miscellaneousRoutes.js | 17 +- server/routes/mixDataRoutes.js | 6 +- server/routes/notificationsRoutes.js | 9 +- server/routes/qboRoutes.js | 14 +- server/routes/renderRoutes.js | 6 +- server/routes/schedulingRoutes.js | 6 +- server/routes/smsRoutes.js | 16 +- server/routes/techRoutes.js | 6 +- server/routes/utilRoutes.js | 8 +- server/utils/adminEmail.js | 13 + 27 files changed, 674 insertions(+), 611 deletions(-) create mode 100644 server/middleware/eventAuthorizationMIddleware.js create mode 100644 server/middleware/validateAdminMiddleware.js create mode 100644 server/middleware/validateFirebaseIdTokenMiddleware.js create mode 100644 server/utils/adminEmail.js diff --git a/server.js b/server.js index 66e665e79..1eaa4b8ec 100644 --- a/server.js +++ b/server.js @@ -51,6 +51,12 @@ app.use(bodyParser.json({limit: "50mb"})); app.use(bodyParser.urlencoded({limit: "50mb", extended: true})); app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]})); +// Helper middleware +app.use((req, res, next) => { + req.logger = logger; + next(); +}); + // Route groupings app.use('/', require("./server/routes/miscellaneousRoutes")); app.use("/notifications", require("./server/routes/notificationsRoutes")); @@ -85,4 +91,4 @@ main() }) .catch((error) => { logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`, "ERROR", "api", error); -}); \ No newline at end of file + }); \ No newline at end of file diff --git a/server/email/sendemail.js b/server/email/sendemail.js index 640d24f2e..170504ce2 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -1,269 +1,269 @@ const path = require("path"); require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), }); const axios = require("axios"); let nodemailer = require("nodemailer"); let aws = require("@aws-sdk/client-ses"); -let { defaultProvider } = require("@aws-sdk/credential-provider-node"); +let {defaultProvider} = require("@aws-sdk/credential-provider-node"); const logger = require("../utils/logger"); const client = require("../graphql-client/graphql-client").client; const queries = require("../graphql-client/queries"); const ses = new aws.SES({ - // The key apiVersion is no longer supported in v3, and can be removed. - // @deprecated The client uses the "latest" apiVersion. - apiVersion: "latest", - region: "ca-central-1", - defaultProvider + // The key apiVersion is no longer supported in v3, and can be removed. + // @deprecated The client uses the "latest" apiVersion. + apiVersion: "latest", + region: "ca-central-1", + defaultProvider }); let transporter = nodemailer.createTransport({ - SES: { ses, aws }, + SES: {ses, aws}, }); -exports.sendServerEmail = async function ({ subject, text }) { - if (process.env.NODE_ENV === undefined) return; - try { - transporter.sendMail( - { - from: `ImEX Online API - ${process.env.NODE_ENV} `, - to: ["patrick@imexsystems.ca", "support@thinkimex.com"], - subject: subject, - text: text, - ses: { - // optional extra arguments for SendRawEmail - Tags: [ +exports.sendServerEmail = async function ({subject, text}) { + if (process.env.NODE_ENV === undefined) return; + try { + transporter.sendMail( { - Name: "tag_name", - Value: "tag_value", + from: `ImEX Online API - ${process.env.NODE_ENV} `, + to: ["patrick@imexsystems.ca", "support@thinkimex.com"], + subject: subject, + text: text, + ses: { + // optional extra arguments for SendRawEmail + Tags: [ + { + Name: "tag_name", + Value: "tag_value", + }, + ], + }, }, - ], - }, - }, - (err, info) => { - console.log(err || info); - } - ); - } catch (error) { - console.log(error); - logger.log("server-email-failure", "error", null, null, error); - } + (err, info) => { + console.log(err || info); + } + ); + } catch (error) { + console.log(error); + logger.log("server-email-failure", "error", null, null, error); + } }; -exports.sendTaskEmail = async function ({ to, subject, text, attachments }) { - try { - transporter.sendMail( - { - from: `ImEX Online `, - to: to, - subject: subject, - text: text, - attachments: attachments || null, - }, - (err, info) => { - console.log(err || info); - } - ); - } catch (error) { - console.log(error); - logger.log("server-email-failure", "error", null, null, error); - } +exports.sendTaskEmail = async function ({to, subject, text, attachments}) { + try { + transporter.sendMail( + { + from: `ImEX Online `, + to: to, + subject: subject, + text: text, + attachments: attachments || null, + }, + (err, info) => { + console.log(err || info); + } + ); + } catch (error) { + console.log(error); + logger.log("server-email-failure", "error", null, null, error); + } }; exports.sendEmail = async (req, res) => { - logger.log("send-email", "DEBUG", req.user.email, null, { - from: `${req.body.from.name} <${req.body.from.address}>`, - replyTo: req.body.ReplyTo.Email, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - }); + logger.log("send-email", "DEBUG", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + replyTo: req.body.ReplyTo.Email, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + }); - let downloadedMedia = []; - if (req.body.media && req.body.media.length > 0) { - downloadedMedia = await Promise.all( - req.body.media.map((m) => { - try { - return getImage(m); - } catch (error) { - logger.log("send-email-error", "ERROR", req.user.email, null, { + let downloadedMedia = []; + if (req.body.media && req.body.media.length > 0) { + downloadedMedia = await Promise.all( + req.body.media.map((m) => { + try { + return getImage(m); + } catch (error) { + logger.log("send-email-error", "ERROR", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + replyTo: req.body.ReplyTo.Email, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + error, + }); + } + }) + ); + } + + transporter.sendMail( + { from: `${req.body.from.name} <${req.body.from.address}>`, replyTo: req.body.ReplyTo.Email, to: req.body.to, cc: req.body.cc, subject: req.body.subject, - error, - }); + attachments: + [ + ...((req.body.attachments && + req.body.attachments.map((a) => { + return { + filename: a.filename, + path: a.path, + }; + })) || + []), + ...downloadedMedia.map((a) => { + return { + path: a, + }; + }), + ] || null, + html: req.body.html, + ses: { + // optional extra arguments for SendRawEmail + Tags: [ + { + Name: "tag_name", + Value: "tag_value", + }, + ], + }, + }, + (err, info) => { + console.log(err || info); + if (info) { + logger.log("send-email-success", "DEBUG", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + replyTo: req.body.ReplyTo.Email, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + // info, + }); + logEmail(req, { + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + messageId: info.response, + }); + res.json({ + success: true, //response: info + }); + } else { + logger.log("send-email-failure", "ERROR", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + replyTo: req.body.ReplyTo.Email, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + error: err, + }); + logEmail(req, { + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + bodyshopid: req.body.bodyshopid, + }); + res.status(500).json({success: false, error: err}); + } } - }) ); - } - - transporter.sendMail( - { - from: `${req.body.from.name} <${req.body.from.address}>`, - replyTo: req.body.ReplyTo.Email, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - attachments: - [ - ...((req.body.attachments && - req.body.attachments.map((a) => { - return { - filename: a.filename, - path: a.path, - }; - })) || - []), - ...downloadedMedia.map((a) => { - return { - path: a, - }; - }), - ] || null, - html: req.body.html, - ses: { - // optional extra arguments for SendRawEmail - Tags: [ - { - Name: "tag_name", - Value: "tag_value", - }, - ], - }, - }, - (err, info) => { - console.log(err || info); - if (info) { - logger.log("send-email-success", "DEBUG", req.user.email, null, { - from: `${req.body.from.name} <${req.body.from.address}>`, - replyTo: req.body.ReplyTo.Email, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - // info, - }); - logEmail(req, { - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - messageId: info.response, - }); - res.json({ - success: true, //response: info - }); - } else { - logger.log("send-email-failure", "ERROR", req.user.email, null, { - from: `${req.body.from.name} <${req.body.from.address}>`, - replyTo: req.body.ReplyTo.Email, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - error: err, - }); - logEmail(req, { - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - bodyshopid: req.body.bodyshopid, - }); - res.status(500).json({ success: false, error: err }); - } - } - ); }; async function getImage(imageUrl) { - let image = await axios.get(imageUrl, { responseType: "arraybuffer" }); - let raw = Buffer.from(image.data).toString("base64"); - return "data:" + image.headers["content-type"] + ";base64," + raw; + let image = await axios.get(imageUrl, {responseType: "arraybuffer"}); + let raw = Buffer.from(image.data).toString("base64"); + return "data:" + image.headers["content-type"] + ";base64," + raw; } async function logEmail(req, email) { - try { - const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, { - email: { - to: email.to, - cc: email.cc, - subject: email.subject, - bodyshopid: req.body.bodyshopid, - useremail: req.user.email, - contents: req.body.html, - jobid: req.body.jobid, - sesmessageid: email.messageId, - status: "Sent", - }, - }); - console.log(insertresult); - } catch (error) { - logger.log("email-log-error", "error", req.user.email, null, { - from: `${req.body.from.name} <${req.body.from.address}>`, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - // info, - }); - } + try { + const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, { + email: { + to: email.to, + cc: email.cc, + subject: email.subject, + bodyshopid: req.body.bodyshopid, + useremail: req.user.email, + contents: req.body.html, + jobid: req.body.jobid, + sesmessageid: email.messageId, + status: "Sent", + }, + }); + console.log(insertresult); + } catch (error) { + logger.log("email-log-error", "error", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + // info, + }); + } } -exports.emailBounce = async function (req, res, next) { - try { - const body = JSON.parse(req.body); - if (body.Type === "SubscriptionConfirmation") { - logger.log("SNS-message", "DEBUG", "api", null, { - body: req.body, - }); - } - const message = JSON.parse(body.Message); - if (message.notificationType === "Bounce") { - let replyTo, subject, messageId; - message.mail.headers.forEach((header) => { - if (header.name === "Reply-To") { - replyTo = header.value; - } else if (header.name === "Subject") { - subject = header.value; +exports.emailBounce = async function (req, res) { + try { + const body = JSON.parse(req.body); + if (body.Type === "SubscriptionConfirmation") { + logger.log("SNS-message", "DEBUG", "api", null, { + body: req.body, + }); } - }); - messageId = message.mail.messageId; - if (replyTo === "noreply@imex.online") { - res.sendStatus(200); - return; - } - //If it's bounced, log it as bounced in audit log. Send an email to the user. - const result = await client.request(queries.UPDATE_EMAIL_AUDIT, { - sesid: messageId, - status: "Bounced", - context: message.bounce?.bouncedRecipients, - }); - transporter.sendMail( - { - from: `ImEX Online `, - to: replyTo, - //bcc: "patrick@snapt.ca", - subject: `ImEX Online Bounced Email - RE: ${subject}`, - text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. + const message = JSON.parse(body.Message); + if (message.notificationType === "Bounce") { + let replyTo, subject, messageId; + message.mail.headers.forEach((header) => { + if (header.name === "Reply-To") { + replyTo = header.value; + } else if (header.name === "Subject") { + subject = header.value; + } + }); + messageId = message.mail.messageId; + if (replyTo === "noreply@imex.online") { + res.sendStatus(200); + return; + } + //If it's bounced, log it as bounced in audit log. Send an email to the user. + const result = await client.request(queries.UPDATE_EMAIL_AUDIT, { + sesid: messageId, + status: "Bounced", + context: message.bounce?.bouncedRecipients, + }); + transporter.sendMail( + { + from: `ImEX Online `, + to: replyTo, + //bcc: "patrick@snapt.ca", + subject: `ImEX Online Bounced Email - RE: ${subject}`, + text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. ${body.bounce?.bouncedRecipients.map( - (r) => - `Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode} + (r) => + `Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode} ` -)} + )} `, - }, - (err, info) => { - console.log("***", err || info); + }, + (err, info) => { + console.log("***", err || info); + } + ); } - ); + } catch (error) { + logger.log("sns-error", "ERROR", "api", null, { + error: JSON.stringify(error), + }); } - } catch (error) { - logger.log("sns-error", "ERROR", "api", null, { - error: JSON.stringify(error), - }); - } - res.sendStatus(200); + res.sendStatus(200); }; diff --git a/server/firebase/firebase-handler.js b/server/firebase/firebase-handler.js index e203bcbf3..d8cf63fb6 100644 --- a/server/firebase/firebase-handler.js +++ b/server/firebase/firebase-handler.js @@ -1,287 +1,215 @@ -var admin = require("firebase-admin"); +const admin = require("firebase-admin"); const logger = require("../utils/logger"); const path = require("path"); -const { auth } = require("firebase-admin"); +const {auth} = require("firebase-admin"); + require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), }); const client = require("../graphql-client/graphql-client").client; -var serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON); + +const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON); +const adminEmail = require("../utils/adminEmail"); admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), - databaseURL: process.env.FIREBASE_DATABASE_URL, + credential: admin.credential.cert(serviceAccount), + databaseURL: process.env.FIREBASE_DATABASE_URL, }); exports.admin = admin; -const adminEmail = [ - "patrick@imex.dev", - //"patrick@imex.test", - "patrick@imex.prod", - "patrick@imexsystems.ca", - "patrick@thinkimex.com", -]; - exports.createUser = async (req, res) => { - logger.log("admin-create-user", "ADMIN", req.user.email, null, { - request: req.body, - ioadmin: true, - }); + logger.log("admin-create-user", "ADMIN", req.user.email, null, { + request: req.body, + ioadmin: true, + }); - const { email, displayName, password, shopid, authlevel } = req.body; - try { - const userRecord = await admin - .auth() - .createUser({ email, displayName, password }); + const {email, displayName, password, shopid, authlevel} = req.body; + try { + const userRecord = await admin + .auth() + .createUser({email, displayName, password}); - // See the UserRecord reference doc for the contents of userRecord. + // See the UserRecord reference doc for the contents of userRecord. - const result = await client.request( - ` + const result = await client.request( + ` mutation INSERT_USER($user: users_insert_input!) { insert_users_one(object: $user) { email } } `, - { - user: { - email: email.toLowerCase(), - authid: userRecord.uid, - associations: { - data: [{ shopid, authlevel, active: true }], - }, - }, - } - ); + { + user: { + email: email.toLowerCase(), + authid: userRecord.uid, + associations: { + data: [{shopid, authlevel, active: true}], + }, + }, + } + ); - res.json({ userRecord, result }); - } catch (error) { - logger.log("admin-update-user-error", "ERROR", req.user.email, null, { - error, - }); - res.status(500).json(error); - } + res.json({userRecord, result}); + } catch (error) { + logger.log("admin-update-user-error", "ERROR", req.user.email, null, { + error, + }); + res.status(500).json(error); + } }; exports.updateUser = (req, res) => { - logger.log("admin-update-user", "ADMIN", req.user.email, null, { - request: req.body, - ioadmin: true, - }); - - if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { - logger.log( - "admin-update-user-unauthorized", - "ERROR", - req.user.email, - null, - { + logger.log("admin-update-user", "ADMIN", req.user.email, null, { request: req.body, - user: req.user, - } - ); - res.sendStatus(404); - return; - } - - admin - .auth() - .updateUser( - req.body.uid, - req.body.user - // { - // email: "modifiedUser@example.com", - // phoneNumber: "+11234567890", - // emailVerified: true, - // password: "newPassword", - // displayName: "Jane Doe", - // photoURL: "http://www.example.com/12345678/photo.png", - // disabled: true, - // } - ) - .then((userRecord) => { - // See the UserRecord reference doc for the contents of userRecord. - - logger.log("admin-update-user-success", "ADMIN", req.user.email, null, { - userRecord, ioadmin: true, - }); - res.json(userRecord); - }) - .catch((error) => { - logger.log("admin-update-user-error", "ERROR", req.user.email, null, { - error, - }); - res.status(500).json(error); }); + + if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { + logger.log( + "admin-update-user-unauthorized", + "ERROR", + req.user.email, + null, + { + request: req.body, + user: req.user, + } + ); + res.sendStatus(404); + return; + } + + admin + .auth() + .updateUser( + req.body.uid, + req.body.user + // { + // email: "modifiedUser@example.com", + // phoneNumber: "+11234567890", + // emailVerified: true, + // password: "newPassword", + // displayName: "Jane Doe", + // photoURL: "http://www.example.com/12345678/photo.png", + // disabled: true, + // } + ) + .then((userRecord) => { + // See the UserRecord reference doc for the contents of userRecord. + + logger.log("admin-update-user-success", "ADMIN", req.user.email, null, { + userRecord, + ioadmin: true, + }); + res.json(userRecord); + }) + .catch((error) => { + logger.log("admin-update-user-error", "ERROR", req.user.email, null, { + error, + }); + res.status(500).json(error); + }); }; exports.getUser = (req, res) => { - logger.log("admin-get-user", "ADMIN", req.user.email, null, { - request: req.body, - ioadmin: true, - }); - - if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { - logger.log( - "admin-update-user-unauthorized", - "ERROR", - req.user.email, - null, - { + logger.log("admin-get-user", "ADMIN", req.user.email, null, { request: req.body, - user: req.user, - } - ); - res.sendStatus(404); - return; - } - - admin - .auth() - .getUser(req.body.uid) - .then((userRecord) => { - res.json(userRecord); - }) - .catch((error) => { - logger.log("admin-get-user-error", "ERROR", req.user.email, null, { - error, - }); - res.status(500).json(error); + ioadmin: true, }); + + if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { + logger.log( + "admin-update-user-unauthorized", + "ERROR", + req.user.email, + null, + { + request: req.body, + user: req.user, + } + ); + res.sendStatus(404); + return; + } + + admin + .auth() + .getUser(req.body.uid) + .then((userRecord) => { + res.json(userRecord); + }) + .catch((error) => { + logger.log("admin-get-user-error", "ERROR", req.user.email, null, { + error, + }); + res.status(500).json(error); + }); }; exports.sendNotification = async (req, res) => { - setTimeout(() => { - // Send a message to the device corresponding to the provided - // registration token. - admin - .messaging() - .send({ - topic: "PRD_PATRICK-messaging", - notification: { - title: `ImEX Online Message - +16049992002`, - body: "Test Noti.", - //imageUrl: "https://thinkimex.com/img/io-fcm.png", - }, - data: { - type: "messaging-inbound", - conversationid: "e0eb17c3-3a78-4e3f-b932-55ef35aa2297", - text: "Hello. ", - image_path: "", - phone_num: "+16049992002", - }, - }) - .then((response) => { - // Response is a message ID string. - console.log("Successfully sent message:", response); - }) - .catch((error) => { - console.log("Error sending message:", error); - }); + setTimeout(() => { + // Send a message to the device corresponding to the provided + // registration token. + admin + .messaging() + .send({ + topic: "PRD_PATRICK-messaging", + notification: { + title: `ImEX Online Message - +16049992002`, + body: "Test Noti.", + //imageUrl: "https://thinkimex.com/img/io-fcm.png", + }, + data: { + type: "messaging-inbound", + conversationid: "e0eb17c3-3a78-4e3f-b932-55ef35aa2297", + text: "Hello. ", + image_path: "", + phone_num: "+16049992002", + }, + }) + .then((response) => { + // Response is a message ID string. + console.log("Successfully sent message:", response); + }) + .catch((error) => { + console.log("Error sending message:", error); + }); - res.sendStatus(200); - }, 500); + res.sendStatus(200); + }, 500); }; exports.subscribe = async (req, res) => { - const result = await admin - .messaging() - .subscribeToTopic( - req.body.fcm_tokens, - `${req.body.imexshopid}-${req.body.type}` - ); + const result = await admin + .messaging() + .subscribeToTopic( + req.body.fcm_tokens, + `${req.body.imexshopid}-${req.body.type}` + ); - res.json(result); + res.json(result); }; exports.unsubscribe = async (req, res) => { - try { - const result = await admin - .messaging() - .unsubscribeFromTopic( - req.body.fcm_tokens, - `${req.body.imexshopid}-${req.body.type}` - ); + try { + const result = await admin + .messaging() + .unsubscribeFromTopic( + req.body.fcm_tokens, + `${req.body.imexshopid}-${req.body.type}` + ); - res.json(result); - } catch (error) { - res.sendStatus(500); - } + res.json(result); + } catch (error) { + res.sendStatus(500); + } }; -exports.validateFirebaseIdToken = async (req, res, next) => { - if ( - (!req.headers.authorization || - !req.headers.authorization.startsWith("Bearer ")) && - !(req.cookies && req.cookies.__session) - ) { - console.error("Unauthorized attempt. No authorization provided."); - res.status(403).send("Unauthorized"); - return; - } - - let idToken; - if ( - req.headers.authorization && - req.headers.authorization.startsWith("Bearer ") - ) { - // console.log('Found "Authorization" header'); - // Read the ID Token from the Authorization header. - idToken = req.headers.authorization.split("Bearer ")[1]; - } else if (req.cookies) { - //console.log('Found "__session" cookie'); - // Read the ID Token from cookie. - idToken = req.cookies.__session; - } else { - // No cookie - console.error("Unauthorized attempt. No cookie provided."); - logger.log("api-unauthorized-call", "WARN", null, null, { - req, - type: "no-cookie", - }); - res.status(403).send("Unauthorized"); - return; - } - - try { - const decodedIdToken = await admin.auth().verifyIdToken(idToken); - //console.log("ID Token correctly decoded", decodedIdToken); - req.user = decodedIdToken; - next(); - return; - } catch (error) { - logger.log("api-unauthorized-call", "WARN", null, null, { - path: req.path, - body: req.body, - - type: "unauthroized", - ...error, - }); - - res.status(401).send("Unauthorized"); - return; - } -}; - -exports.validateAdmin = async (req, res, next) => { - if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { - logger.log("admin-validation-failed", "ERROR", req.user.email, null, { - request: req.body, - user: req.user, - }); - res.sendStatus(404); - return; - } else { - next(); - return; - } -}; //Admin claims code. // const uid = "JEqqYlsadwPEXIiyRBR55fflfko1"; diff --git a/server/job/job-costing.js b/server/job/job-costing.js index e7568fa72..4c23538cc 100644 --- a/server/job/job-costing.js +++ b/server/job/job-costing.js @@ -13,6 +13,7 @@ async function JobCosting(req, res) { const { jobid } = req.body; const BearerToken = req.headers.authorization; + logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null); const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { headers: { diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 3834bf1cb..61e3703fb 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,5 +1,17 @@ +const _ = require("lodash"); const jobLifecycle = (req, res) => { - return res.status(200).send("jobLifecycle"); + const {jobids} = req.body; + return _.isArray(jobids) ? + handleMultipleJobs(jobids, req, res) : + handleSingleJob(jobids, req, res); }; +const handleMultipleJobs = (jobIDs, req, res) => { + return res.status(200).send(jobIDs); +} + +const handleSingleJob = (req, res) => { + return res.status(200).send(req.body); +} + module.exports = jobLifecycle; \ No newline at end of file diff --git a/server/job/job-status-transition.js b/server/job/job-status-transition.js index 294ab395d..960041746 100644 --- a/server/job/job-status-transition.js +++ b/server/job/job-status-transition.js @@ -9,89 +9,84 @@ const logger = require("../utils/logger"); Dinero.globalRoundingMode = "HALF_EVEN"; const path = require("path"); const client = require("../graphql-client/graphql-client").client; + require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), }); + async function StatusTransition(req, res) { - if (req.headers["event-secret"] !== process.env.EVENT_SECRET) { - res.status(401).send("Unauthorized"); - return; - } - - // return res.sendStatus(200); - - const { - id: jobid, - status: value, - shopid: bodyshopid, - } = req.body.event.data.new; + const { + id: jobid, + status: value, + shopid: bodyshopid, + } = req.body.event.data.new; // Create record OPEN on new item, enter state // If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status // (Timeline) // Final status is exported, there is no end date as there is no further transition (has no end date) - try { - const { update_transitions } = await client.request( - queries.UPDATE_OLD_TRANSITION, - { - jobid: jobid, - existingTransition: { - end: new Date(), - next_value: value, + try { + const {update_transitions} = await client.request( + queries.UPDATE_OLD_TRANSITION, + { + jobid: jobid, + existingTransition: { + end: new Date(), + next_value: value, - //duration - }, - } - ); + //duration + }, + } + ); - let duration = - update_transitions.affected_rows === 0 - ? 0 - : new Date(update_transitions.returning[0].end) - - new Date(update_transitions.returning[0].start); + let duration = + update_transitions.affected_rows === 0 + ? 0 + : new Date(update_transitions.returning[0].end) - + new Date(update_transitions.returning[0].start); - const resp2 = await client.request(queries.INSERT_NEW_TRANSITION, { - oldTransitionId: - update_transitions.affected_rows === 0 - ? null - : update_transitions.returning[0].id, - duration, - newTransition: { - bodyshopid: bodyshopid, - jobid: jobid, - start: - update_transitions.affected_rows === 0 - ? new Date() - : update_transitions.returning[0].end, - prev_value: - update_transitions.affected_rows === 0 - ? null - : update_transitions.returning[0].value, - value: value, - type: "status", - }, - }); + const resp2 = await client.request(queries.INSERT_NEW_TRANSITION, { + oldTransitionId: + update_transitions.affected_rows === 0 + ? null + : update_transitions.returning[0].id, + duration, + newTransition: { + bodyshopid: bodyshopid, + jobid: jobid, + start: + update_transitions.affected_rows === 0 + ? new Date() + : update_transitions.returning[0].end, + prev_value: + update_transitions.affected_rows === 0 + ? null + : update_transitions.returning[0].value, + value: value, + type: "status", + }, + }); - //Check to see if there is an existing status transition record. - //Query using Job ID, start is not null, end is null. + //Check to see if there is an existing status transition record. + //Query using Job ID, start is not null, end is null. - //If there is no existing record, this is the start of the transition life cycle. - // Create the initial transition record. + //If there is no existing record, this is the start of the transition life cycle. + // Create the initial transition record. - //If there is a current status transition record, update it with the end date, duration, and next value. + //If there is a current status transition record, update it with the end date, duration, and next value. - res.sendStatus(200); //.json(ret); - } catch (error) { - logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, { - message: error.message, - stack: error.stack, - }); + res.sendStatus(200); //.json(ret); + } catch (error) { + logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, { + message: error.message, + stack: error.stack, + }); - res.status(400).send(JSON.stringify(error)); - } + res.status(400).send(JSON.stringify(error)); + } } exports.statustransition = StatusTransition; diff --git a/server/middleware/eventAuthorizationMIddleware.js b/server/middleware/eventAuthorizationMIddleware.js new file mode 100644 index 000000000..8579a2e89 --- /dev/null +++ b/server/middleware/eventAuthorizationMIddleware.js @@ -0,0 +1,15 @@ +/** + * Checks if the event secret is correct + * @param req + * @param res + * @param next + */ +function eventAuthorizationMiddleware(req, res, next) { + if (req.headers["event-secret"] !== process.env.EVENT_SECRET) { + return res.status(401).send("Unauthorized"); + } + + next(); +} + +module.exports = eventAuthorizationMiddleware; \ No newline at end of file diff --git a/server/middleware/validateAdminMiddleware.js b/server/middleware/validateAdminMiddleware.js new file mode 100644 index 000000000..a93c7d659 --- /dev/null +++ b/server/middleware/validateAdminMiddleware.js @@ -0,0 +1,15 @@ +const logger = require("../utils/logger"); +const adminEmail = require("../utils/adminEmail"); + +const validateAdminMiddleware = (req, res, next) => { + if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { + logger.log("admin-validation-failed", "ERROR", req.user.email, null, { + request: req.body, + user: req.user, + }); + return res.sendStatus(404); + } + next(); +}; + +module.exports = validateAdminMiddleware; \ No newline at end of file diff --git a/server/middleware/validateFirebaseIdTokenMiddleware.js b/server/middleware/validateFirebaseIdTokenMiddleware.js new file mode 100644 index 000000000..a9522a0bb --- /dev/null +++ b/server/middleware/validateFirebaseIdTokenMiddleware.js @@ -0,0 +1,59 @@ +const logger = require("../utils/logger"); +const admin = require("firebase-admin"); + +const validateFirebaseIdTokenMiddleware = async (req, res, next) => { + if ( + (!req.headers.authorization || + !req.headers.authorization.startsWith("Bearer ")) && + !(req.cookies && req.cookies.__session) + ) { + console.error("Unauthorized attempt. No authorization provided."); + res.status(403).send("Unauthorized"); + return; + } + + let idToken; + + if ( + req.headers.authorization && + req.headers.authorization.startsWith("Bearer ") + ) { + // console.log('Found "Authorization" header'); + // Read the ID Token from the Authorization header. + idToken = req.headers.authorization.split("Bearer ")[1]; + } else if (req.cookies) { + //console.log('Found "__session" cookie'); + // Read the ID Token from cookie. + idToken = req.cookies.__session; + } else { + // No cookie + console.error("Unauthorized attempt. No cookie provided."); + logger.log("api-unauthorized-call", "WARN", null, null, { + req, + type: "no-cookie", + }); + res.status(403).send("Unauthorized"); + return; + } + + try { + const decodedIdToken = await admin.auth().verifyIdToken(idToken); + //console.log("ID Token correctly decoded", decodedIdToken); + req.user = decodedIdToken; + next(); + + } catch (error) { + logger.log("api-unauthorized-call", "WARN", null, null, { + path: req.path, + body: req.body, + + type: "unauthroized", + ...error, + }); + + res.status(401).send("Unauthorized"); + + } +}; + +module.exports = validateFirebaseIdTokenMiddleware; \ No newline at end of file diff --git a/server/opensearch/os-handler.js b/server/opensearch/os-handler.js index 7cb544400..2beeaca3a 100644 --- a/server/opensearch/os-handler.js +++ b/server/opensearch/os-handler.js @@ -15,10 +15,6 @@ const {getClient} = require('../../libs/awsUtils'); async function OpenSearchUpdateHandler(req, res) { - if (req.headers["event-secret"] !== process.env.EVENT_SECRET) { - res.status(401).send("Unauthorized"); - return; - } try { const osClient = await getClient(); diff --git a/server/routes/accountingRoutes.js b/server/routes/accountingRoutes.js index 9e390746d..aff49cbdb 100644 --- a/server/routes/accountingRoutes.js +++ b/server/routes/accountingRoutes.js @@ -1,10 +1,12 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -const accountQbxml = require('../accounting/qbxml/qbxml'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {payments, payables, receivables} = require("../accounting/qbxml/qbxml"); -router.post('/qbxml/receivables', fb.validateFirebaseIdToken, accountQbxml.receivables); -router.post('/qbxml/payables', fb.validateFirebaseIdToken, accountQbxml.payables); -router.post('/qbxml/payments', fb.validateFirebaseIdToken, accountQbxml.payments); +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/qbxml/receivables', receivables); +router.post('/qbxml/payables', payables); +router.post('/qbxml/payments', payments); module.exports = router; diff --git a/server/routes/adminRoutes.js b/server/routes/adminRoutes.js index 9fffd1568..617f343b3 100644 --- a/server/routes/adminRoutes.js +++ b/server/routes/adminRoutes.js @@ -1,15 +1,18 @@ const express = require('express'); const router = express.Router(); const fb = require('../firebase/firebase-handler'); -const adm = require('../admin/adminops'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {createAssociation, createShop, updateShop, updateCounter} = require("../admin/adminops"); +const validateAdminMiddleware = require("../middleware/validateAdminMiddleware"); +router.use(validateFirebaseIdTokenMiddleware); -router.post('/createassociation', fb.validateFirebaseIdToken, fb.validateAdmin, adm.createAssociation); -router.post('/createshop', fb.validateFirebaseIdToken, fb.validateAdmin, adm.createShop); -router.post('/updateshop', fb.validateFirebaseIdToken, fb.validateAdmin, adm.updateShop); -router.post('/updatecounter', fb.validateFirebaseIdToken, fb.validateAdmin, adm.updateCounter); -router.post('/updateuser', fb.validateFirebaseIdToken, fb.updateUser); -router.post('/getuser', fb.validateFirebaseIdToken, fb.getUser); -router.post('/createuser', fb.validateFirebaseIdToken, fb.createUser); +router.post('/createassociation', validateAdminMiddleware, createAssociation); +router.post('/createshop', validateAdminMiddleware, createShop); +router.post('/updateshop', validateAdminMiddleware, updateShop); +router.post('/updatecounter', validateAdminMiddleware, updateCounter); +router.post('/updateuser', fb.updateUser); +router.post('/getuser', fb.getUser); +router.post('/createuser', fb.createUser); module.exports = router; diff --git a/server/routes/cdkRoutes.js b/server/routes/cdkRoutes.js index 9aa234655..fdedadf92 100644 --- a/server/routes/cdkRoutes.js +++ b/server/routes/cdkRoutes.js @@ -1,8 +1,10 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); const cdkGetMake = require('../cdk/cdk-get-makes'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); -router.post('/getvehicles', fb.validateFirebaseIdToken, cdkGetMake.default); +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/getvehicles', cdkGetMake.default); module.exports = router; diff --git a/server/routes/dataRoutes.js b/server/routes/dataRoutes.js index 7a6c631ff..0240f3388 100644 --- a/server/routes/dataRoutes.js +++ b/server/routes/dataRoutes.js @@ -1,9 +1,9 @@ const express = require('express'); const router = express.Router(); -const data = require('../data/data'); +const {autohouse, claimscorp, kaizen} = require('../data/data'); -router.post('/ah', data.autohouse); -router.post('/cc', data.claimscorp); -router.post('/kaizen', data.kaizen); +router.post('/ah', autohouse); +router.post('/cc', claimscorp); +router.post('/kaizen', kaizen); module.exports = router; diff --git a/server/routes/intellipayRoutes.js b/server/routes/intellipayRoutes.js index 88144f1be..3952a3450 100644 --- a/server/routes/intellipayRoutes.js +++ b/server/routes/intellipayRoutes.js @@ -1,11 +1,11 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -const intellipay = require('../intellipay/intellipay'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {lightbox_credentials, payment_refund, generate_payment_url, postback} = require("../intellipay/intellipay"); -router.post('/lightbox_credentials', fb.validateFirebaseIdToken, intellipay.lightbox_credentials); -router.post('/payment_refund', fb.validateFirebaseIdToken, intellipay.payment_refund); -router.post('/generate_payment_url', fb.validateFirebaseIdToken, intellipay.generate_payment_url); -router.post('/postback', intellipay.postback); +router.post('/lightbox_credentials', validateFirebaseIdTokenMiddleware, lightbox_credentials); +router.post('/payment_refund', validateFirebaseIdTokenMiddleware, payment_refund); +router.post('/generate_payment_url', validateFirebaseIdTokenMiddleware, generate_payment_url); +router.post('/postback', postback); module.exports = router; diff --git a/server/routes/jobRoutes.js b/server/routes/jobRoutes.js index f3cc7bf1b..dea660200 100644 --- a/server/routes/jobRoutes.js +++ b/server/routes/jobRoutes.js @@ -1,15 +1,19 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); const job = require('../job/job'); -const partsScan = require('../parts-scan/parts-scan'); +const {partsScan} = require('../parts-scan/parts-scan'); +const eventAuthorizationMiddleware = require('../middleware/eventAuthorizationMIddleware'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {totals, statustransition, totalsSsu, costing, lifecycle, costingmulti} = require("../job/job"); -router.post('/totals', fb.validateFirebaseIdToken, job.totals); -router.post('/statustransition', fb.validateFirebaseIdToken, job.statustransition); -router.post('/totalsssu', fb.validateFirebaseIdToken, job.totalsSsu); -router.post('/costing', fb.validateFirebaseIdToken, job.costing); -router.get('/lifecycle', fb.validateFirebaseIdToken, job.lifecycle); -router.post('/costingmulti', fb.validateFirebaseIdToken, job.costingmulti); -router.post('/partsscan', fb.validateFirebaseIdToken, partsScan.partsScan); +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/totals', totals); +router.post('/statustransition', eventAuthorizationMiddleware, statustransition); +router.post('/totalsssu', totalsSsu); +router.post('/costing', costing); +router.get('/lifecycle', lifecycle); +router.post('/costingmulti', costingmulti); +router.post('/partsscan', partsScan); module.exports = router; diff --git a/server/routes/mediaRoutes.js b/server/routes/mediaRoutes.js index 3d6d9f69e..a44a1a048 100644 --- a/server/routes/mediaRoutes.js +++ b/server/routes/mediaRoutes.js @@ -1,11 +1,13 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -const media = require('../media/media'); +const {createSignedUploadURL, downloadFiles, renameKeys, deleteFiles} = require('../media/media'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); -router.post('/sign', fb.validateFirebaseIdToken, media.createSignedUploadURL); -router.post('/download', fb.validateFirebaseIdToken, media.downloadFiles); -router.post('/rename', fb.validateFirebaseIdToken, media.renameKeys); -router.post('/delete', fb.validateFirebaseIdToken, media.deleteFiles); +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/sign', createSignedUploadURL); +router.post('/download', downloadFiles); +router.post('/rename', renameKeys); +router.post('/delete', deleteFiles); module.exports = router; diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js index bae1a255c..fcdc23398 100644 --- a/server/routes/miscellaneousRoutes.js +++ b/server/routes/miscellaneousRoutes.js @@ -3,11 +3,12 @@ const router = express.Router(); const logger = require("../../server/utils/logger"); const sendEmail = require("../email/sendemail"); const data = require("../data/data"); -const fb = require("../firebase/firebase-handler"); const bodyParser = require("body-parser"); const ioevent = require("../ioevent/ioevent"); const taskHandler = require("../tasks/tasks"); const os = require("../opensearch/os-handler"); +const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware"); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); //Test route to ensure Express is responding. router.get("/test", async function (req, res) { @@ -29,15 +30,21 @@ router.get("/test", async function (req, res) { res.status(200).send(`OK - ${commit}`); }); -router.post("/search", fb.validateFirebaseIdToken, os.search); -router.post("/opensearch", os.handler); +// Search +router.post("/search", validateFirebaseIdTokenMiddleware, os.search); +router.post("/opensearch", eventAuthorizationMiddleware, os.handler); +// IO Events router.post('/ioevent', ioevent.default); -router.post('/sendemail', fb.validateFirebaseIdToken, sendEmail.sendEmail); + +// Email +router.post('/sendemail', validateFirebaseIdTokenMiddleware, sendEmail.sendEmail); router.post('/emailbounce', bodyParser.text(), sendEmail.emailBounce); + +// Handlers router.post('/record-handler/arms', data.arms); -router.post("/taskHandler", fb.validateFirebaseIdToken, taskHandler.taskHandler); +router.post("/taskHandler", validateFirebaseIdTokenMiddleware, taskHandler.taskHandler); module.exports = router; diff --git a/server/routes/mixDataRoutes.js b/server/routes/mixDataRoutes.js index 67559bd9a..f3f4d8afe 100644 --- a/server/routes/mixDataRoutes.js +++ b/server/routes/mixDataRoutes.js @@ -2,9 +2,9 @@ const express = require('express'); const router = express.Router(); const multer = require('multer'); const upload = multer(); -const fb = require('../firebase/firebase-handler'); -const mixdataUpload = require('../mixdata/mixdata'); +const {mixdataUpload} = require('../mixdata/mixdata'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); -router.post('/upload', fb.validateFirebaseIdToken, upload.any(), mixdataUpload.mixdataUpload); +router.post('/upload', validateFirebaseIdTokenMiddleware, upload.any(), mixdataUpload); module.exports = router; diff --git a/server/routes/notificationsRoutes.js b/server/routes/notificationsRoutes.js index b1c5ac48e..1a8e9de7b 100644 --- a/server/routes/notificationsRoutes.js +++ b/server/routes/notificationsRoutes.js @@ -1,8 +1,11 @@ const express = require('express'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {subscribe, unsubscribe} = require("../firebase/firebase-handler"); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -router.post('/subscribe', fb.validateFirebaseIdToken, fb.subscribe); -router.post('/unsubscribe', fb.validateFirebaseIdToken, fb.unsubscribe); +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/subscribe', subscribe); +router.post('/unsubscribe', unsubscribe); module.exports = router; diff --git a/server/routes/qboRoutes.js b/server/routes/qboRoutes.js index 69cb3edb9..e7a00619f 100644 --- a/server/routes/qboRoutes.js +++ b/server/routes/qboRoutes.js @@ -1,13 +1,13 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -const qbo = require('../accounting/qbo/qbo'); // Assuming you have a qbo module for handling QuickBooks Online related functionalities +const {authorize, callback, receivables, payables, payments} = require('../accounting/qbo/qbo'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); // Assuming you have a qbo module for handling QuickBooks Online related functionalities // Define the routes for QuickBooks Online -router.post('/authorize', fb.validateFirebaseIdToken, qbo.authorize); -router.get('/callback', qbo.callback); -router.post('/receivables', fb.validateFirebaseIdToken, qbo.receivables); -router.post('/payables', fb.validateFirebaseIdToken, qbo.payables); -router.post('/payments', fb.validateFirebaseIdToken, qbo.payments); +router.post('/authorize', validateFirebaseIdTokenMiddleware, authorize); +router.get('/callback', callback); +router.post('/receivables', validateFirebaseIdTokenMiddleware, receivables); +router.post('/payables', validateFirebaseIdTokenMiddleware, payables); +router.post('/payments', validateFirebaseIdTokenMiddleware, payments); module.exports = router; diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js index 87dd75e70..7242404e5 100644 --- a/server/routes/renderRoutes.js +++ b/server/routes/renderRoutes.js @@ -1,9 +1,9 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -const inlineCss = require('../render/inlinecss'); +const {inlinecss} = require('../render/inlinecss'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); // Define the route for inline CSS rendering -router.post('/inlinecss', fb.validateFirebaseIdToken, inlineCss.inlinecss); +router.post('/inlinecss', validateFirebaseIdTokenMiddleware, inlinecss); module.exports = router; diff --git a/server/routes/schedulingRoutes.js b/server/routes/schedulingRoutes.js index 36a934102..38a91229b 100644 --- a/server/routes/schedulingRoutes.js +++ b/server/routes/schedulingRoutes.js @@ -1,8 +1,8 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -const scheduling = require('../scheduling/scheduling-job'); +const {job} = require('../scheduling/scheduling-job'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); -router.post('/job', fb.validateFirebaseIdToken, scheduling.job); +router.post('/job', validateFirebaseIdTokenMiddleware, job); module.exports = router; diff --git a/server/routes/smsRoutes.js b/server/routes/smsRoutes.js index a642c61bf..9952e0d64 100644 --- a/server/routes/smsRoutes.js +++ b/server/routes/smsRoutes.js @@ -1,17 +1,17 @@ const express = require('express'); const router = express.Router(); const twilio = require('twilio'); -const fb = require('../firebase/firebase-handler'); -const smsReceive = require('../sms/receive'); -const smsSend = require('../sms/send'); -const smsStatus = require('../sms/status'); +const {receive} = require('../sms/receive'); +const {send} = require('../sms/send'); +const {status, markConversationRead} = require('../sms/status'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); // Twilio Webhook Middleware for production const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }); -router.post('/receive', twilioWebhookMiddleware, smsReceive.receive); -router.post('/send', fb.validateFirebaseIdToken, smsSend.send); -router.post('/status', twilioWebhookMiddleware, smsStatus.status); -router.post('/markConversationRead', fb.validateFirebaseIdToken, smsStatus.markConversationRead); +router.post('/receive', twilioWebhookMiddleware, receive); +router.post('/send', validateFirebaseIdTokenMiddleware, send); +router.post('/status', twilioWebhookMiddleware, status); +router.post('/markConversationRead', validateFirebaseIdTokenMiddleware, markConversationRead); module.exports = router; diff --git a/server/routes/techRoutes.js b/server/routes/techRoutes.js index ca6d4c936..e7594f532 100644 --- a/server/routes/techRoutes.js +++ b/server/routes/techRoutes.js @@ -1,8 +1,8 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -const tech = require('../tech/tech'); +const {techLogin} = require('../tech/tech'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); -router.post('/login', fb.validateFirebaseIdToken, tech.techLogin); +router.post('/login', validateFirebaseIdTokenMiddleware, techLogin); module.exports = router; diff --git a/server/routes/utilRoutes.js b/server/routes/utilRoutes.js index 173637479..938b1e1b1 100644 --- a/server/routes/utilRoutes.js +++ b/server/routes/utilRoutes.js @@ -1,9 +1,9 @@ const express = require('express'); const router = express.Router(); -const fb = require('../firebase/firebase-handler'); -const utils = require('../utils/utils'); +const {servertime, jsrAuth} = require('../utils/utils'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); -router.post('/time', utils.servertime); -router.post('/jsr', fb.validateFirebaseIdToken, utils.jsrAuth); +router.post('/time', servertime); +router.post('/jsr', validateFirebaseIdTokenMiddleware, jsrAuth); module.exports = router; diff --git a/server/utils/adminEmail.js b/server/utils/adminEmail.js new file mode 100644 index 000000000..70c44cb47 --- /dev/null +++ b/server/utils/adminEmail.js @@ -0,0 +1,13 @@ +/** + * List of admin email addresses + * @type {string[]} + */ +const adminEmail = [ + "patrick@imex.dev", + //"patrick@imex.test", + "patrick@imex.prod", + "patrick@imexsystems.ca", + "patrick@thinkimex.com", +]; + +module.exports = adminEmail; \ No newline at end of file From a162b275a3f43df0ad3aaeb29a5ad4cbc4c059d0 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Mon, 22 Jan 2024 23:11:10 -0500 Subject: [PATCH 13/48] - Finish cleanup Signed-off-by: Dave Richer --- .../eventAuthorizationMIddleware.js | 3 +++ server/middleware/validateAdminMiddleware.js | 11 ++++++++ .../validateFirebaseIdTokenMiddleware.js | 26 +++++++++++++------ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/server/middleware/eventAuthorizationMIddleware.js b/server/middleware/eventAuthorizationMIddleware.js index 8579a2e89..c766ceda5 100644 --- a/server/middleware/eventAuthorizationMIddleware.js +++ b/server/middleware/eventAuthorizationMIddleware.js @@ -1,5 +1,7 @@ /** * Checks if the event secret is correct + * It adds the following properties to the request object: + * - req.isEventAuthorized - Returns true if the event secret is correct * @param req * @param res * @param next @@ -9,6 +11,7 @@ function eventAuthorizationMiddleware(req, res, next) { return res.status(401).send("Unauthorized"); } + req.isEventAuthorized = true; next(); } diff --git a/server/middleware/validateAdminMiddleware.js b/server/middleware/validateAdminMiddleware.js index a93c7d659..cfd53b171 100644 --- a/server/middleware/validateAdminMiddleware.js +++ b/server/middleware/validateAdminMiddleware.js @@ -1,6 +1,15 @@ const logger = require("../utils/logger"); const adminEmail = require("../utils/adminEmail"); +/** + * Validate admin middleware + * It adds the following properties to the request object: + * - req.isAdmin - returns true if the user passed an admin check + * @param req + * @param res + * @param next + * @returns {*} + */ const validateAdminMiddleware = (req, res, next) => { if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { logger.log("admin-validation-failed", "ERROR", req.user.email, null, { @@ -9,6 +18,8 @@ const validateAdminMiddleware = (req, res, next) => { }); return res.sendStatus(404); } + + req.isAdmin = true; next(); }; diff --git a/server/middleware/validateFirebaseIdTokenMiddleware.js b/server/middleware/validateFirebaseIdTokenMiddleware.js index a9522a0bb..53d1cc775 100644 --- a/server/middleware/validateFirebaseIdTokenMiddleware.js +++ b/server/middleware/validateFirebaseIdTokenMiddleware.js @@ -1,15 +1,26 @@ const logger = require("../utils/logger"); const admin = require("firebase-admin"); +/** + * Middleware to validate Firebase ID Tokens. + * This middleware is used to protect API endpoints from unauthorized access. + * It adds the following properties to the request object: + * - req.user - the decoded Firebase ID Token + * @param req + * @param res + * @param next + * @returns {Promise} + */ const validateFirebaseIdTokenMiddleware = async (req, res, next) => { if ( - (!req.headers.authorization || + ( + !req.headers.authorization || !req.headers.authorization.startsWith("Bearer ")) && - !(req.cookies && req.cookies.__session) + !(req.cookies && req.cookies.__session + ) ) { console.error("Unauthorized attempt. No authorization provided."); - res.status(403).send("Unauthorized"); - return; + return res.status(403).send("Unauthorized"); } let idToken; @@ -32,8 +43,8 @@ const validateFirebaseIdTokenMiddleware = async (req, res, next) => { req, type: "no-cookie", }); - res.status(403).send("Unauthorized"); - return; + + return res.status(403).send("Unauthorized"); } try { @@ -51,8 +62,7 @@ const validateFirebaseIdTokenMiddleware = async (req, res, next) => { ...error, }); - res.status(401).send("Unauthorized"); - + return res.status(401).send("Unauthorized"); } }; From 52f8eabd2bf4372209b7654eb331dd3a40664f4a Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 00:02:18 -0500 Subject: [PATCH 14/48] - Finish cleanup Signed-off-by: Dave Richer --- server/accounting/qbo/qbo-payables.js | 18 +- server/accounting/qbo/qbo-payments.js | 12 +- server/accounting/qbo/qbo-receivables.js | 13 +- server/accounting/qbxml/qbxml-payables.js | 13 +- server/accounting/qbxml/qbxml-payments.js | 13 +- server/accounting/qbxml/qbxml-receivables.js | 13 +- server/cdk/cdk-get-makes.js | 10 +- server/job/job-costing.js | 1840 ++++++++--------- server/job/job-lifecycle.js | 4 + server/job/job-totals.js | 28 +- .../withUserGraphQLClientMiddleware.js | 24 + server/mixdata/mixdata.js | 12 +- server/opensearch/os-handler.js | 9 +- server/parts-scan/parts-scan.js | 14 +- server/routes/accountingRoutes.js | 7 +- server/routes/cdkRoutes.js | 3 +- server/routes/jobRoutes.js | 13 +- server/routes/miscellaneousRoutes.js | 5 +- server/routes/mixDataRoutes.js | 3 +- server/routes/qboRoutes.js | 9 +- server/routes/schedulingRoutes.js | 3 +- server/scheduling/scheduling-job.js | 12 +- 22 files changed, 1033 insertions(+), 1045 deletions(-) create mode 100644 server/middleware/withUserGraphQLClientMiddleware.js diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 18ab2f8f8..3679db86e 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -18,10 +18,10 @@ const { } = require("./qbo-callback"); const OAuthClient = require("intuit-oauth"); const moment = require("moment-timezone"); -const GraphQLClient = require("graphql-request").GraphQLClient; const findTaxCode = require("../qb-receivables-lines").findTaxCode; exports.default = async (req, res) => { + const oauthClient = new OAuthClient({ clientId: process.env.QBO_CLIENT_ID, clientSecret: process.env.QBO_SECRET, @@ -30,29 +30,31 @@ exports.default = async (req, res) => { redirectUri: process.env.QBO_REDIRECT_URI, logging: true, }); + try { //Fetch the API Access Tokens & Set them for the session. const response = await apiGqlClient.request(queries.GET_QBO_AUTH, { email: req.user.email, }); + const { qbo_realmId } = response.associations[0]; oauthClient.setToken(response.associations[0].qbo_auth); + if (!qbo_realmId) { res.status(401).json({ error: "No company associated." }); return; } + await refreshOauthToken(oauthClient, req); - const BearerToken = req.headers.authorization; const { bills: billsToQuery, elgen } = req.body; - //Query Job Info - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("qbo-payable-create", "DEBUG", req.user.email, billsToQuery); + const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, { diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js index 13f15e2a6..15418606c 100644 --- a/server/accounting/qbo/qbo-payments.js +++ b/server/accounting/qbo/qbo-payments.js @@ -51,15 +51,13 @@ exports.default = async (req, res) => { } await refreshOauthToken(oauthClient, req); - const BearerToken = req.headers.authorization; const { payments: paymentsToQuery, elgen } = req.body; - //Query Job Info - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("qbo-payment-create", "DEBUG", req.user.email, paymentsToQuery); + const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_PAYMENTS_FOR_EXPORT, { diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index a09c22d80..fdd7660d8 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -18,8 +18,6 @@ const { const OAuthClient = require("intuit-oauth"); const CreateInvoiceLines = require("../qb-receivables-lines").default; const moment = require("moment-timezone"); - -const GraphQLClient = require("graphql-request").GraphQLClient; const { generateOwnerTier } = require("../qbxml/qbxml-utils"); const { createMultiQbPayerLines } = require("../qb-receivables-lines"); @@ -46,15 +44,14 @@ exports.default = async (req, res) => { await refreshOauthToken(oauthClient, req); - const BearerToken = req.headers.authorization; const { jobIds, elgen } = req.body; //Query Job Info - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("qbo-receivable-create", "DEBUG", req.user.email, jobIds); + const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { diff --git a/server/accounting/qbxml/qbxml-payables.js b/server/accounting/qbxml/qbxml-payables.js index 26f0f2d8a..21cc519b5 100644 --- a/server/accounting/qbxml/qbxml-payables.js +++ b/server/accounting/qbxml/qbxml-payables.js @@ -3,10 +3,11 @@ const path = require("path"); const DineroQbFormat = require("../accounting-constants").DineroQbFormat; const queries = require("../../graphql-client/queries"); const Dinero = require("dinero.js"); -var builder = require("xmlbuilder2"); +const builder = require("xmlbuilder2"); const QbXmlUtils = require("./qbxml-utils"); const moment = require("moment-timezone"); -const logger = require("../../utils/logger"); +const logger = require('../../utils/logger'); + require("dotenv").config({ path: path.resolve( process.cwd(), @@ -15,14 +16,10 @@ require("dotenv").config({ }); exports.default = async (req, res) => { - const BearerToken = req.headers.authorization; const { bills: billsToQuery } = req.body; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; try { logger.log( diff --git a/server/accounting/qbxml/qbxml-payments.js b/server/accounting/qbxml/qbxml-payments.js index 263b1533e..8e563c9ac 100644 --- a/server/accounting/qbxml/qbxml-payments.js +++ b/server/accounting/qbxml/qbxml-payments.js @@ -1,13 +1,12 @@ -const GraphQLClient = require("graphql-request").GraphQLClient; const path = require("path"); const DineroQbFormat = require("../accounting-constants").DineroQbFormat; const queries = require("../../graphql-client/queries"); const Dinero = require("dinero.js"); -var builder = require("xmlbuilder2"); +const builder = require("xmlbuilder2"); const moment = require("moment-timezone"); const QbXmlUtils = require("./qbxml-utils"); const QbxmlReceivables = require("./qbxml-receivables"); -const logger = require("../../utils/logger"); +const logger = require('../../utils/logger'); require("dotenv").config({ path: path.resolve( @@ -19,14 +18,10 @@ require("dotenv").config({ const { generateJobTier, generateOwnerTier, generateSourceTier } = QbXmlUtils; exports.default = async (req, res) => { - const BearerToken = req.headers.authorization; const { payments: paymentsToQuery } = req.body; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; try { logger.log( diff --git a/server/accounting/qbxml/qbxml-receivables.js b/server/accounting/qbxml/qbxml-receivables.js index 5489f859e..1d55cee4d 100644 --- a/server/accounting/qbxml/qbxml-receivables.js +++ b/server/accounting/qbxml/qbxml-receivables.js @@ -1,13 +1,12 @@ -const GraphQLClient = require("graphql-request").GraphQLClient; const path = require("path"); const DineroQbFormat = require("../accounting-constants").DineroQbFormat; const queries = require("../../graphql-client/queries"); const Dinero = require("dinero.js"); const moment = require("moment-timezone"); -var builder = require("xmlbuilder2"); +const builder = require("xmlbuilder2"); const QbXmlUtils = require("./qbxml-utils"); -const logger = require("../../utils/logger"); const CreateInvoiceLines = require("../qb-receivables-lines").default; +const logger = require('../../utils/logger'); require("dotenv").config({ path: path.resolve( @@ -20,14 +19,10 @@ Dinero.globalRoundingMode = "HALF_EVEN"; const { generateJobTier, generateOwnerTier, generateSourceTier } = QbXmlUtils; exports.default = async (req, res) => { - const BearerToken = req.headers.authorization; const { jobIds } = req.body; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; try { logger.log( diff --git a/server/cdk/cdk-get-makes.js b/server/cdk/cdk-get-makes.js index dc5c59801..0ca36f9e0 100644 --- a/server/cdk/cdk-get-makes.js +++ b/server/cdk/cdk-get-makes.js @@ -5,7 +5,6 @@ require("dotenv").config({ `.env.${process.env.NODE_ENV || "development"}` ), }); -const GraphQLClient = require("graphql-request").GraphQLClient; const soap = require("soap"); const queries = require("../graphql-client/queries"); @@ -34,16 +33,11 @@ const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl"); exports.default = async function ReloadCdkMakes(req, res) { const { bodyshopid, cdk_dealerid } = req.body; try { - const BearerToken = req.headers.authorization; //Query all CDK Models const newList = await GetCdkMakes(req, cdk_dealerid); - //Clear out the existing records - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; const deleteResult = await client .setHeaders({ Authorization: BearerToken }) diff --git a/server/job/job-costing.js b/server/job/job-costing.js index 4c23538cc..4e7b40dc7 100644 --- a/server/job/job-costing.js +++ b/server/job/job-costing.js @@ -1,84 +1,752 @@ +const _ = require("lodash"); const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); -//const client = require("../graphql-client/graphql-client").client; -const _ = require("lodash"); -const GraphQLClient = require("graphql-request").GraphQLClient; -const logger = require("../utils/logger"); -const { DiscountNotAlreadyCounted } = require("./job-totals"); +const logger = require('../utils/logger'); +const {DiscountNotAlreadyCounted} = require("./job-totals"); + // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; Dinero.globalRoundingMode = "HALF_EVEN"; async function JobCosting(req, res) { - const { jobid } = req.body; + const {jobid} = req.body; - const BearerToken = req.headers.authorization; + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; - logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null); - try { - const resp = await client - .setHeaders({ Authorization: BearerToken }) - .request(queries.QUERY_JOB_COSTING_DETAILS, { - id: jobid, - }); + try { + const resp = await client + .setHeaders({Authorization: BearerToken}) + .request(queries.QUERY_JOB_COSTING_DETAILS, { + id: jobid, + }); - const ret = GenerateCostingData(resp.jobs_by_pk); + const ret = GenerateCostingData(resp.jobs_by_pk); - res.status(200).json(ret); - } catch (error) { - logger.log("job-costing-error", "ERROR", req.user.email, jobid, { - message: error.message, - stack: error.stack, - }); + res.status(200).json(ret); + } catch (error) { + logger.log("job-costing-error", "ERROR", req.user.email, jobid, { + message: error.message, + stack: error.stack, + }); - res.status(400).send(JSON.stringify(error)); - } + res.status(400).send(JSON.stringify(error)); + } } async function JobCostingMulti(req, res) { - const { jobids } = req.body; - const BearerToken = req.headers.authorization; - logger.log("job-costing-multi-start", "DEBUG", req.user.email, jobids, null); + const {jobids} = req.body; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const logger = req.logger; + const BearerToken = req.BearerToken + const client = req.userGraphQLClient; - try { - const resp = await client - .setHeaders({ Authorization: BearerToken }) - .request(queries.QUERY_JOB_COSTING_DETAILS_MULTI, { - ids: jobids, - }); + logger.log("job-costing-multi-start", "DEBUG", req.user.email, jobids, null); - const multiSummary = { - costCenterData: [], - summaryData: { - totalLaborSales: Dinero({ amount: 0 }), - totalPartsSales: Dinero({ amount: 0 }), - totalAdditionalSales: Dinero({ amount: 0 }), - totalSubletSales: Dinero({ amount: 0 }), - totalSales: Dinero({ amount: 0 }), - totalLaborCost: Dinero({ amount: 0 }), - totalPartsCost: Dinero({ amount: 0 }), - totalAdditionalCost: Dinero({ amount: 0 }), - totalSubletCost: Dinero({ amount: 0 }), - totalCost: Dinero({ amount: 0 }), - gpdollars: Dinero({ amount: 0 }), - gppercent: null, - gppercentFormatted: null, - totalLaborGp: Dinero({ amount: 0 }), - totalPartsGp: Dinero({ amount: 0 }), - totalAdditionalGp: Dinero({ amount: 0 }), - totalSubletGp: Dinero({ amount: 0 }), + + try { + const resp = await client + .setHeaders({Authorization: BearerToken}) + .request(queries.QUERY_JOB_COSTING_DETAILS_MULTI, { + ids: jobids, + }); + + const multiSummary = { + costCenterData: [], + summaryData: { + totalLaborSales: Dinero({amount: 0}), + totalPartsSales: Dinero({amount: 0}), + totalAdditionalSales: Dinero({amount: 0}), + totalSubletSales: Dinero({amount: 0}), + totalSales: Dinero({amount: 0}), + totalLaborCost: Dinero({amount: 0}), + totalPartsCost: Dinero({amount: 0}), + totalAdditionalCost: Dinero({amount: 0}), + totalSubletCost: Dinero({amount: 0}), + totalCost: Dinero({amount: 0}), + gpdollars: Dinero({amount: 0}), + gppercent: null, + gppercentFormatted: null, + totalLaborGp: Dinero({amount: 0}), + totalPartsGp: Dinero({amount: 0}), + totalAdditionalGp: Dinero({amount: 0}), + totalSubletGp: Dinero({amount: 0}), + totalLaborGppercent: null, + totalLaborGppercentFormatted: null, + totalPartsGppercent: null, + totalPartsGppercentFormatted: null, + totalAdditionalGppercent: null, + totalAdditionalGppercentFormatted: null, + totalSubletGppercent: null, + totalSubletGppercentFormatted: null, + }, + }; + + const ret = {}; + resp.jobs.map((job) => { + const costingData = GenerateCostingData(job); + ret[job.id] = costingData; + + //Merge on a cost center basis. + + costingData.costCenterData.forEach((c) => { + //Find the Cost Center if it exists. + + const CostCenterIndex = multiSummary.costCenterData.findIndex( + (x) => x.cost_center === c.cost_center + ); + + if (CostCenterIndex >= 0) { + //Add it in place + multiSummary.costCenterData[CostCenterIndex] = { + ...multiSummary.costCenterData[CostCenterIndex], + sale_labor_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sale_labor_dinero.add(c.sale_labor_dinero), + sale_parts_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sale_parts_dinero.add(c.sale_parts_dinero), + sale_additional_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sale_additional_dinero.add(c.sale_additional_dinero), + sale_sublet_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sale_sublet_dinero.add(c.sale_sublet_dinero), + cost_labor_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].cost_labor_dinero.add(c.cost_labor_dinero), + cost_parts_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].cost_parts_dinero.add(c.cost_parts_dinero), + cost_additional_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].cost_additional_dinero.add(c.cost_additional_dinero), + cost_sublet_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].cost_sublet_dinero.add(c.cost_sublet_dinero), + gpdollars_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].gpdollars_dinero.add(c.gpdollars_dinero), + costs_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].costs_dinero.add(c.costs_dinero), + sales_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sales_dinero.add(c.sales_dinero), + }; + } else { + //Add it to the list instead. + multiSummary.costCenterData.push(c); + } + }); + + //Add all summary data. + multiSummary.summaryData.totalPartsSales = + multiSummary.summaryData.totalPartsSales.add( + costingData.summaryData.totalPartsSales + ); + multiSummary.summaryData.totalAdditionalSales = + multiSummary.summaryData.totalAdditionalSales.add( + costingData.summaryData.totalAdditionalSales + ); + multiSummary.summaryData.totalSubletSales = + multiSummary.summaryData.totalSubletSales.add( + costingData.summaryData.totalSubletSales + ); + multiSummary.summaryData.totalSales = + multiSummary.summaryData.totalSales.add( + costingData.summaryData.totalSales + ); + multiSummary.summaryData.totalLaborCost = + multiSummary.summaryData.totalLaborCost.add( + costingData.summaryData.totalLaborCost + ); + multiSummary.summaryData.totalLaborSales = + multiSummary.summaryData.totalLaborSales.add( + costingData.summaryData.totalLaborSales + ); + multiSummary.summaryData.totalPartsCost = + multiSummary.summaryData.totalPartsCost.add( + costingData.summaryData.totalPartsCost + ); + multiSummary.summaryData.totalAdditionalCost = + multiSummary.summaryData.totalAdditionalCost.add( + costingData.summaryData.totalAdditionalCost + ); + multiSummary.summaryData.totalSubletCost = + multiSummary.summaryData.totalSubletCost.add( + costingData.summaryData.totalSubletCost + ); + multiSummary.summaryData.totalCost = + multiSummary.summaryData.totalCost.add( + costingData.summaryData.totalCost + ); + multiSummary.summaryData.gpdollars = + multiSummary.summaryData.gpdollars.add( + costingData.summaryData.gpdollars + ); + + multiSummary.summaryData.totalLaborGp = + multiSummary.summaryData.totalLaborGp.add( + costingData.summaryData.totalLaborGp + ); + multiSummary.summaryData.totalPartsGp = + multiSummary.summaryData.totalPartsGp.add( + costingData.summaryData.totalPartsGp + ); + multiSummary.summaryData.totalAdditionalGp = + multiSummary.summaryData.totalAdditionalGp.add( + costingData.summaryData.totalAdditionalGp + ); + multiSummary.summaryData.totalSubletGp = + multiSummary.summaryData.totalSubletGp.add( + costingData.summaryData.totalSubletGp + ); + + //Take the summary data & add it to total summary data. + }); + + //For each center, recalculate and toFormat() the values. + + multiSummary.summaryData.totalLaborGppercent = ( + (multiSummary.summaryData.totalLaborGp.getAmount() / + multiSummary.summaryData.totalLaborSales.getAmount()) * + 100 + ).toFixed(1); + multiSummary.summaryData.totalLaborGppercentFormatted = formatGpPercent( + multiSummary.summaryData.totalLaborGppercent + ); + + multiSummary.summaryData.totalPartsGppercent = ( + (multiSummary.summaryData.totalPartsGp.getAmount() / + multiSummary.summaryData.totalPartsSales.getAmount()) * + 100 + ).toFixed(1); + + multiSummary.summaryData.totalPartsGppercentFormatted = formatGpPercent( + multiSummary.summaryData.totalPartsGppercent + ); + + multiSummary.summaryData.totalAdditionalGppercent = ( + (multiSummary.summaryData.totalAdditionalGp.getAmount() / + multiSummary.summaryData.totalAdditionalSales.getAmount()) * + 100 + ).toFixed(1); + + multiSummary.summaryData.totalAdditionalGppercentFormatted = + formatGpPercent(multiSummary.summaryData.totalAdditionalGppercent); + + multiSummary.summaryData.totalSubletGppercent = ( + (multiSummary.summaryData.totalSubletGp.getAmount() / + multiSummary.summaryData.totalSubletSales.getAmount()) * + 100 + ).toFixed(1); + + multiSummary.summaryData.totalSubletGppercentFormatted = formatGpPercent( + multiSummary.summaryData.totalSubletGppercent + ); + + multiSummary.summaryData.gppercent = ( + (multiSummary.summaryData.gpdollars.getAmount() / + multiSummary.summaryData.totalSales.getAmount()) * + 100 + ).toFixed(1); + + multiSummary.summaryData.gppercentFormatted = formatGpPercent( + multiSummary.summaryData.gppercent + ); + + const finalCostingdata = multiSummary.costCenterData.map((c) => { + return { + ...c, + sale_labor: c.sale_labor_dinero && c.sale_labor_dinero.toFormat(), + sale_parts: c.sale_parts_dinero && c.sale_parts_dinero.toFormat(), + sale_additional: + c.sale_additional_dinero && c.sale_additional_dinero.toFormat(), + sale_sublet: c.sale_sublet_dinero && c.sale_sublet_dinero.toFormat(), + sales: c.sales_dinero.toFormat(), + cost_parts: c.cost_parts_dinero && c.cost_parts_dinero.toFormat(), + cost_labor: c.cost_labor_dinero && c.cost_labor_dinero.toFormat(), + cost_additional: + c.cost_additional_dinero && c.cost_additional_dinero.toFormat(), + cost_sublet: c.cost_sublet_dinero && c.cost_sublet_dinero.toFormat(), + costs: c.costs_dinero.toFormat(), + gpdollars: c.gpdollars_dinero.toFormat(), + gppercent: formatGpPercent( + ( + (c.gpdollars_dinero.getAmount() / c.sales_dinero.getAmount()) * + 100 + ).toFixed(1) + ), + }; + }); + + //Calculate thte total gross profit percentages. + + res.status(200).json({ + allCostCenterData: finalCostingdata, + allSummaryData: multiSummary.summaryData, + data: ret, + }); + } catch (error) { + logger.log("job-costing-multi-error", "ERROR", req.user.email, [jobids], { + message: error.message, + stack: error.stack, + }); + res.status(400).send(error); + } +} + +function GenerateCostingData(job) { + const defaultProfits = + job.bodyshop.md_responsibility_centers.defaults.profits; + const allCenters = _.union( + job.bodyshop.md_responsibility_centers.profits.map((p) => p.name), + job.bodyshop.md_responsibility_centers.costs.map((p) => p.name), + ["Unknown"] + ); + + const materialsHours = {mapaHrs: 0, mashHrs: 0}; + let hasMapaLine = false; + let hasMashLine = false; + + //Massage the data. + const jobLineTotalsByProfitCenter = + job && + job.joblines.reduce( + (acc, val) => { + //Parts Lines + if (val.db_ref === "936008") { + //If either of these DB REFs change, they also need to change in job-totals/job-costing calculations. + hasMapaLine = true; + } + if (val.db_ref === "936007") { + hasMashLine = true; + } + if (val.mod_lbr_ty) { + const laborProfitCenter = + val.profitcenter_labor || + defaultProfits[val.mod_lbr_ty] || + "Unknown"; + + if (laborProfitCenter === "Unknown") + console.log("Unknown type", val.line_desc, val.mod_lbr_ty); + + const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`; + + const laborAmount = Dinero({ + amount: Math.round((job[rateName] || 0) * 100), + }).multiply(val.mod_lb_hrs || 0); + if (!acc.labor[laborProfitCenter]) + acc.labor[laborProfitCenter] = Dinero(); + acc.labor[laborProfitCenter] = + acc.labor[laborProfitCenter].add(laborAmount); + + if ( + val.mod_lb_hrs === 0 && + val.act_price > 0 && + val.lbr_op === "OP14" + ) { + //Scenario where SGI may pay out hours using a part price. + acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add( + Dinero({ + amount: Math.round((val.act_price || 0) * 100), + }).multiply(val.part_qty) + ); + } + + if (val.mod_lbr_ty === "LAR") { + materialsHours.mapaHrs += val.mod_lb_hrs || 0; + } + if (val.mod_lbr_ty !== "LAR") { + materialsHours.mashHrs += val.mod_lb_hrs || 0; + } + } + + if ( + val.part_type && + val.part_type !== "PAE" && + val.part_type !== "PAS" && + val.part_type !== "PASL" + ) { + const partsProfitCenter = + val.profitcenter_part || defaultProfits[val.part_type] || "Unknown"; + + if (partsProfitCenter === "Unknown") + console.log("Unknown type", val.line_desc, val.part_type); + + if (!partsProfitCenter) + console.log( + "Unknown cost/profit center mapping for parts.", + val.line_desc, + val.part_type + ); + const partsAmount = Dinero({ + amount: Math.round((val.act_price || 0) * 100), + }) + .multiply(val.part_qty || 1) + .add( + ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || + (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && + DiscountNotAlreadyCounted(val, job.joblines) + ? val.prt_dsmk_m + ? Dinero({amount: Math.round(val.prt_dsmk_m * 100)}) + : Dinero({ + amount: Math.round(val.act_price * 100), + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1) + : Dinero() + ); + if (!acc.parts[partsProfitCenter]) + acc.parts[partsProfitCenter] = Dinero(); + acc.parts[partsProfitCenter] = + acc.parts[partsProfitCenter].add(partsAmount); + } + if ( + val.part_type && + val.part_type !== "PAE" && + (val.part_type === "PAS" || val.part_type === "PASL") + ) { + const partsProfitCenter = + val.profitcenter_part || defaultProfits[val.part_type] || "Unknown"; + + if (partsProfitCenter === "Unknown") + console.log("Unknown type", val.line_desc, val.part_type); + + if (!partsProfitCenter) + console.log( + "Unknown cost/profit center mapping for sublet.", + val.line_desc, + val.part_type + ); + const partsAmount = Dinero({ + amount: Math.round((val.act_price || 0) * 100), + }) + .multiply(val.part_qty || 1) + .add( + ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || + (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && + DiscountNotAlreadyCounted(val, job.joblines) + ? val.prt_dsmk_m + ? Dinero({amount: Math.round(val.prt_dsmk_m * 100)}) + : Dinero({ + amount: Math.round(val.act_price * 100), + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1) + : Dinero() + ); + if (!acc.sublet[partsProfitCenter]) + acc.sublet[partsProfitCenter] = Dinero(); + acc.sublet[partsProfitCenter] = + acc.sublet[partsProfitCenter].add(partsAmount); + } + + //To deal with additional costs. + if (!val.part_type && !val.mod_lbr_ty) { + //Does it already have a defined profit center? + //If so, use it, otherwise try to use the same from the auto-allocate logic in IO app jobs-close-auto-allocate. + const partsProfitCenter = + val.profitcenter_part || + getAdditionalCostCenter(val, defaultProfits) || + "Unknown"; + + if (partsProfitCenter === "Unknown") { + console.log("Unknown type", val.line_desc, val.part_type); + } + const partsAmount = Dinero({ + amount: Math.round((val.act_price || 0) * 100), + }) + .multiply(val.part_qty || 1) + .add( + ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || + (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && + DiscountNotAlreadyCounted(val, job.joblines) + ? val.prt_dsmk_m + ? Dinero({amount: Math.round(val.prt_dsmk_m * 100)}) + : Dinero({ + amount: Math.round(val.act_price * 100), + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1) + : Dinero() + ); + + if (!acc.additional[partsProfitCenter]) + acc.additional[partsProfitCenter] = Dinero(); + acc.additional[partsProfitCenter] = + acc.additional[partsProfitCenter].add(partsAmount); + } + + return acc; + }, + {parts: {}, labor: {}, additional: {}, sublet: {}} + ); + + if (!hasMapaLine) { + if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]]) + jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = Dinero(); + jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = + jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]].add( + Dinero({ + amount: Math.round((job.rate_mapa || 0) * 100), + }).multiply(materialsHours.mapaHrs || 0) + ); + } + if (!hasMashLine) { + if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]]) + jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = Dinero(); + jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = + jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]].add( + Dinero({ + amount: Math.round((job.rate_mash || 0) * 100), + }).multiply(materialsHours.mashHrs || 0) + ); + } + + //Is it a DMS Setup? + const selectedDmsAllocationConfig = + (job.bodyshop.md_responsibility_centers.dms_defaults && + job.bodyshop.md_responsibility_centers.dms_defaults.find( + (d) => d.name === job.dms_allocation + )) || + job.bodyshop.md_responsibility_centers.defaults; + + const billTotalsByCostCenters = job.bills.reduce( + (bill_acc, bill_val) => { + //At the bill level. + bill_val.billlines.map((line_val) => { + //At the bill line level. + if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { + if ( + !bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] + ) + bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = + Dinero(); + + bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = + bill_acc[ + selectedDmsAllocationConfig.costs[line_val.cost_center] + ].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + } else { + const isSubletCostCenter = + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.PAS || + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.PASL; + + const isAdditionalCostCenter = + // line_val.cost_center === + // job.bodyshop.md_responsibility_centers.defaults.costs.PAS || + // line_val.cost_center === + // job.bodyshop.md_responsibility_centers.defaults.costs.PASL || + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.TOW || + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA || + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.MASH; + + if (isAdditionalCostCenter) { + if (!bill_acc.additionalCosts[line_val.cost_center]) + bill_acc.additionalCosts[line_val.cost_center] = Dinero(); + + bill_acc.additionalCosts[line_val.cost_center] = + bill_acc.additionalCosts[line_val.cost_center].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + } else if (isSubletCostCenter) { + if (!bill_acc.subletCosts[line_val.cost_center]) + bill_acc.subletCosts[line_val.cost_center] = Dinero(); + + bill_acc.subletCosts[line_val.cost_center] = bill_acc.subletCosts[ + line_val.cost_center + ].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + } else { + if (!bill_acc[line_val.cost_center]) + bill_acc[line_val.cost_center] = Dinero(); + + bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + } + } + + return null; + }); + return bill_acc; + }, + {additionalCosts: {}, subletCosts: {}} + ); + + //If the hourly rates for job costing are set, add them in. + + if ( + job.bodyshop.jc_hourly_rates && + (job.bodyshop.jc_hourly_rates.mapa || + typeof job.bodyshop.jc_hourly_rates.mapa === "number" || + isNaN(job.bodyshop.jc_hourly_rates.mapa) === false) + ) { + if ( + !billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] + ) + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero(); + if (job.bodyshop.use_paint_scale_data === true) { + if (job.mixdata.length > 0) { + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero({ + amount: Math.round( + ((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100 + ), + }); + } else { + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(materialsHours.mapaHrs) + ); + } + } else { + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(materialsHours.mapaHrs) + ); + } + } + + if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) { + if ( + !billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] + ) + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = Dinero(); + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mash * 100) || + 0 + ), + }).multiply(materialsHours.mashHrs) + ); + } + + const ticketTotalsByCostCenter = job.timetickets.reduce( + (ticket_acc, ticket_val) => { + //At the invoice level. + + if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { + if ( + !ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] + ) + ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = + Dinero(); + + ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = + ticket_acc[ + selectedDmsAllocationConfig.costs[ticket_val.ciecacode] + ].add( + Dinero({ + amount: Math.round((ticket_val.rate || 0) * 100), + }).multiply( + ticket_val.flat_rate + ? ticket_val.productivehrs || ticket_val.actualhrs || 0 + : ticket_val.actualhrs || ticket_val.productivehrs || 0 + ) //Should base this on the employee. + ); + } else { + if (!ticket_acc[ticket_val.cost_center]) + ticket_acc[ticket_val.cost_center] = Dinero(); + + ticket_acc[ticket_val.cost_center] = ticket_acc[ + ticket_val.cost_center + ].add( + Dinero({ + amount: Math.round((ticket_val.rate || 0) * 100), + }).multiply( + ticket_val.flat_rate + ? ticket_val.productivehrs || ticket_val.actualhrs || 0 + : ticket_val.actualhrs || ticket_val.productivehrs || 0 + ) //Should base this on the employee. + ); + } + + return ticket_acc; + }, + {} + ); + + const summaryData = { + totalLaborSales: Dinero({amount: 0}), + totalPartsSales: Dinero({amount: 0}), + totalAdditionalSales: Dinero({amount: 0}), + totalSubletSales: Dinero({amount: 0}), + totalSales: Dinero({amount: 0}), + totalLaborCost: Dinero({amount: 0}), + totalPartsCost: Dinero({amount: 0}), + totalAdditionalCost: Dinero({amount: 0}), + totalSubletCost: Dinero({amount: 0}), + totalCost: Dinero({amount: 0}), + totalLaborGp: Dinero({amount: 0}), + totalPartsGp: Dinero({amount: 0}), + totalAdditionalGp: Dinero({amount: 0}), + totalSubletGp: Dinero({amount: 0}), + gpdollars: Dinero({amount: 0}), totalLaborGppercent: null, totalLaborGppercentFormatted: null, totalPartsGppercent: null, @@ -87,894 +755,220 @@ async function JobCostingMulti(req, res) { totalAdditionalGppercentFormatted: null, totalSubletGppercent: null, totalSubletGppercentFormatted: null, - }, + gppercent: null, + gppercentFormatted: null, }; - const ret = {}; - resp.jobs.map((job) => { - const costingData = GenerateCostingData(job); - ret[job.id] = costingData; + const costCenterData = allCenters.map((key, idx) => { + const ccVal = key; // defaultProfits[key]; + const sale_labor = + jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({amount: 0}); + const sale_parts = + jobLineTotalsByProfitCenter.parts[ccVal] || Dinero({amount: 0}); + const sale_additional = + jobLineTotalsByProfitCenter.additional[ccVal] || Dinero({amount: 0}); + const sale_sublet = + jobLineTotalsByProfitCenter.sublet[ccVal] || Dinero({amount: 0}); - //Merge on a cost center basis. + const cost_labor = ticketTotalsByCostCenter[ccVal] || Dinero({amount: 0}); + const cost_parts = billTotalsByCostCenters[ccVal] || Dinero({amount: 0}); + const cost_additional = + billTotalsByCostCenters.additionalCosts[ccVal] || Dinero({amount: 0}); + const cost_sublet = + billTotalsByCostCenters.subletCosts[ccVal] || Dinero({amount: 0}); - costingData.costCenterData.forEach((c) => { - //Find the Cost Center if it exists. - - const CostCenterIndex = multiSummary.costCenterData.findIndex( - (x) => x.cost_center === c.cost_center - ); - - if (CostCenterIndex >= 0) { - //Add it in place - multiSummary.costCenterData[CostCenterIndex] = { - ...multiSummary.costCenterData[CostCenterIndex], - sale_labor_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sale_labor_dinero.add(c.sale_labor_dinero), - sale_parts_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sale_parts_dinero.add(c.sale_parts_dinero), - sale_additional_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sale_additional_dinero.add(c.sale_additional_dinero), - sale_sublet_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sale_sublet_dinero.add(c.sale_sublet_dinero), - cost_labor_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].cost_labor_dinero.add(c.cost_labor_dinero), - cost_parts_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].cost_parts_dinero.add(c.cost_parts_dinero), - cost_additional_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].cost_additional_dinero.add(c.cost_additional_dinero), - cost_sublet_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].cost_sublet_dinero.add(c.cost_sublet_dinero), - gpdollars_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].gpdollars_dinero.add(c.gpdollars_dinero), - costs_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].costs_dinero.add(c.costs_dinero), - sales_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sales_dinero.add(c.sales_dinero), - }; - } else { - //Add it to the list instead. - multiSummary.costCenterData.push(c); - } - }); - - //Add all summary data. - multiSummary.summaryData.totalPartsSales = - multiSummary.summaryData.totalPartsSales.add( - costingData.summaryData.totalPartsSales - ); - multiSummary.summaryData.totalAdditionalSales = - multiSummary.summaryData.totalAdditionalSales.add( - costingData.summaryData.totalAdditionalSales - ); - multiSummary.summaryData.totalSubletSales = - multiSummary.summaryData.totalSubletSales.add( - costingData.summaryData.totalSubletSales - ); - multiSummary.summaryData.totalSales = - multiSummary.summaryData.totalSales.add( - costingData.summaryData.totalSales - ); - multiSummary.summaryData.totalLaborCost = - multiSummary.summaryData.totalLaborCost.add( - costingData.summaryData.totalLaborCost - ); - multiSummary.summaryData.totalLaborSales = - multiSummary.summaryData.totalLaborSales.add( - costingData.summaryData.totalLaborSales - ); - multiSummary.summaryData.totalPartsCost = - multiSummary.summaryData.totalPartsCost.add( - costingData.summaryData.totalPartsCost - ); - multiSummary.summaryData.totalAdditionalCost = - multiSummary.summaryData.totalAdditionalCost.add( - costingData.summaryData.totalAdditionalCost - ); - multiSummary.summaryData.totalSubletCost = - multiSummary.summaryData.totalSubletCost.add( - costingData.summaryData.totalSubletCost - ); - multiSummary.summaryData.totalCost = - multiSummary.summaryData.totalCost.add( - costingData.summaryData.totalCost - ); - multiSummary.summaryData.gpdollars = - multiSummary.summaryData.gpdollars.add( - costingData.summaryData.gpdollars - ); - - multiSummary.summaryData.totalLaborGp = - multiSummary.summaryData.totalLaborGp.add( - costingData.summaryData.totalLaborGp - ); - multiSummary.summaryData.totalPartsGp = - multiSummary.summaryData.totalPartsGp.add( - costingData.summaryData.totalPartsGp - ); - multiSummary.summaryData.totalAdditionalGp = - multiSummary.summaryData.totalAdditionalGp.add( - costingData.summaryData.totalAdditionalGp - ); - multiSummary.summaryData.totalSubletGp = - multiSummary.summaryData.totalSubletGp.add( - costingData.summaryData.totalSubletGp - ); - - //Take the summary data & add it to total summary data. - }); - - //For each center, recalculate and toFormat() the values. - - multiSummary.summaryData.totalLaborGppercent = ( - (multiSummary.summaryData.totalLaborGp.getAmount() / - multiSummary.summaryData.totalLaborSales.getAmount()) * - 100 - ).toFixed(1); - multiSummary.summaryData.totalLaborGppercentFormatted = formatGpPercent( - multiSummary.summaryData.totalLaborGppercent - ); - - multiSummary.summaryData.totalPartsGppercent = ( - (multiSummary.summaryData.totalPartsGp.getAmount() / - multiSummary.summaryData.totalPartsSales.getAmount()) * - 100 - ).toFixed(1); - - multiSummary.summaryData.totalPartsGppercentFormatted = formatGpPercent( - multiSummary.summaryData.totalPartsGppercent - ); - - multiSummary.summaryData.totalAdditionalGppercent = ( - (multiSummary.summaryData.totalAdditionalGp.getAmount() / - multiSummary.summaryData.totalAdditionalSales.getAmount()) * - 100 - ).toFixed(1); - - multiSummary.summaryData.totalAdditionalGppercentFormatted = - formatGpPercent(multiSummary.summaryData.totalAdditionalGppercent); - - multiSummary.summaryData.totalSubletGppercent = ( - (multiSummary.summaryData.totalSubletGp.getAmount() / - multiSummary.summaryData.totalSubletSales.getAmount()) * - 100 - ).toFixed(1); - - multiSummary.summaryData.totalSubletGppercentFormatted = formatGpPercent( - multiSummary.summaryData.totalSubletGppercent - ); - - multiSummary.summaryData.gppercent = ( - (multiSummary.summaryData.gpdollars.getAmount() / - multiSummary.summaryData.totalSales.getAmount()) * - 100 - ).toFixed(1); - - multiSummary.summaryData.gppercentFormatted = formatGpPercent( - multiSummary.summaryData.gppercent - ); - - const finalCostingdata = multiSummary.costCenterData.map((c) => { - return { - ...c, - sale_labor: c.sale_labor_dinero && c.sale_labor_dinero.toFormat(), - sale_parts: c.sale_parts_dinero && c.sale_parts_dinero.toFormat(), - sale_additional: - c.sale_additional_dinero && c.sale_additional_dinero.toFormat(), - sale_sublet: c.sale_sublet_dinero && c.sale_sublet_dinero.toFormat(), - sales: c.sales_dinero.toFormat(), - cost_parts: c.cost_parts_dinero && c.cost_parts_dinero.toFormat(), - cost_labor: c.cost_labor_dinero && c.cost_labor_dinero.toFormat(), - cost_additional: - c.cost_additional_dinero && c.cost_additional_dinero.toFormat(), - cost_sublet: c.cost_sublet_dinero && c.cost_sublet_dinero.toFormat(), - costs: c.costs_dinero.toFormat(), - gpdollars: c.gpdollars_dinero.toFormat(), - gppercent: formatGpPercent( - ( - (c.gpdollars_dinero.getAmount() / c.sales_dinero.getAmount()) * + const costs = cost_labor + .add(cost_parts) + .add(cost_additional) + .add(cost_sublet); + const totalSales = sale_labor + .add(sale_parts) + .add(sale_additional) + .add(sale_sublet); + const gpdollars = totalSales.subtract(costs); + const gppercent = ( + (gpdollars.getAmount() / Math.abs(totalSales.getAmount())) * 100 - ).toFixed(1) - ), - }; + ).toFixed(1); + + //Push summary data to avoid extra loop. + summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor); + summaryData.totalPartsSales = summaryData.totalPartsSales.add(sale_parts); + summaryData.totalAdditionalSales = + summaryData.totalAdditionalSales.add(sale_additional); + summaryData.totalSubletSales = + summaryData.totalSubletSales.add(sale_sublet); + summaryData.totalSales = summaryData.totalSales.add(totalSales); + summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor); + summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts); + summaryData.totalAdditionalCost = + summaryData.totalAdditionalCost.add(cost_additional); + summaryData.totalSubletCost = summaryData.totalSubletCost.add(cost_sublet); + summaryData.totalCost = summaryData.totalCost.add(costs); + + return { + id: idx, + cost_center: ccVal, + sale_labor: sale_labor && sale_labor.toFormat(), + sale_labor_dinero: sale_labor, + sale_parts: sale_parts && sale_parts.toFormat(), + sale_parts_dinero: sale_parts, + sale_additional: sale_additional && sale_additional.toFormat(), + sale_additional_dinero: sale_additional, + sale_sublet: sale_sublet && sale_sublet.toFormat(), + sale_sublet_dinero: sale_sublet, + sales: totalSales.toFormat(), + sales_dinero: totalSales, + cost_parts: cost_parts && cost_parts.toFormat(), + cost_parts_dinero: cost_parts, + cost_labor: cost_labor && cost_labor.toFormat(), + cost_labor_dinero: cost_labor, + cost_additional: cost_additional && cost_additional.toFormat(), + cost_additional_dinero: cost_additional, + cost_sublet: cost_sublet && cost_sublet.toFormat(), + cost_sublet_dinero: cost_sublet, + costs: costs.toFormat(), + costs_dinero: costs, + gpdollars_dinero: gpdollars, + gpdollars: gpdollars.toFormat(), + gppercent: formatGpPercent(gppercent), + }; }); - //Calculate thte total gross profit percentages. - - res.status(200).json({ - allCostCenterData: finalCostingdata, - allSummaryData: multiSummary.summaryData, - data: ret, - }); - } catch (error) { - logger.log("job-costing-multi-error", "ERROR", req.user.email, [jobids], { - message: error.message, - stack: error.stack, - }); - res.status(400).send(error); - } -} - -function GenerateCostingData(job) { - const defaultProfits = - job.bodyshop.md_responsibility_centers.defaults.profits; - const allCenters = _.union( - job.bodyshop.md_responsibility_centers.profits.map((p) => p.name), - job.bodyshop.md_responsibility_centers.costs.map((p) => p.name), - ["Unknown"] - ); - - const materialsHours = { mapaHrs: 0, mashHrs: 0 }; - let hasMapaLine = false; - let hasMashLine = false; - - //Massage the data. - const jobLineTotalsByProfitCenter = - job && - job.joblines.reduce( - (acc, val) => { - //Parts Lines - if (val.db_ref === "936008") { - //If either of these DB REFs change, they also need to change in job-totals/job-costing calculations. - hasMapaLine = true; - } - if (val.db_ref === "936007") { - hasMashLine = true; - } - if (val.mod_lbr_ty) { - const laborProfitCenter = - val.profitcenter_labor || - defaultProfits[val.mod_lbr_ty] || - "Unknown"; - - if (laborProfitCenter === "Unknown") - console.log("Unknown type", val.line_desc, val.mod_lbr_ty); - - const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`; - - const laborAmount = Dinero({ - amount: Math.round((job[rateName] || 0) * 100), - }).multiply(val.mod_lb_hrs || 0); - if (!acc.labor[laborProfitCenter]) - acc.labor[laborProfitCenter] = Dinero(); - acc.labor[laborProfitCenter] = - acc.labor[laborProfitCenter].add(laborAmount); - - if ( - val.mod_lb_hrs === 0 && - val.act_price > 0 && - val.lbr_op === "OP14" - ) { - //Scenario where SGI may pay out hours using a part price. - acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add( - Dinero({ - amount: Math.round((val.act_price || 0) * 100), - }).multiply(val.part_qty) - ); - } - - if (val.mod_lbr_ty === "LAR") { - materialsHours.mapaHrs += val.mod_lb_hrs || 0; - } - if (val.mod_lbr_ty !== "LAR") { - materialsHours.mashHrs += val.mod_lb_hrs || 0; - } - } - - if ( - val.part_type && - val.part_type !== "PAE" && - val.part_type !== "PAS" && - val.part_type !== "PASL" - ) { - const partsProfitCenter = - val.profitcenter_part || defaultProfits[val.part_type] || "Unknown"; - - if (partsProfitCenter === "Unknown") - console.log("Unknown type", val.line_desc, val.part_type); - - if (!partsProfitCenter) - console.log( - "Unknown cost/profit center mapping for parts.", - val.line_desc, - val.part_type - ); - const partsAmount = Dinero({ - amount: Math.round((val.act_price || 0) * 100), - }) - .multiply(val.part_qty || 1) - .add( - ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || - (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && - DiscountNotAlreadyCounted(val, job.joblines) - ? val.prt_dsmk_m - ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) - : Dinero({ - amount: Math.round(val.act_price * 100), - }) - .multiply(val.part_qty || 0) - .percentage(Math.abs(val.prt_dsmk_p || 0)) - .multiply(val.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() - ); - if (!acc.parts[partsProfitCenter]) - acc.parts[partsProfitCenter] = Dinero(); - acc.parts[partsProfitCenter] = - acc.parts[partsProfitCenter].add(partsAmount); - } - if ( - val.part_type && - val.part_type !== "PAE" && - (val.part_type === "PAS" || val.part_type === "PASL") - ) { - const partsProfitCenter = - val.profitcenter_part || defaultProfits[val.part_type] || "Unknown"; - - if (partsProfitCenter === "Unknown") - console.log("Unknown type", val.line_desc, val.part_type); - - if (!partsProfitCenter) - console.log( - "Unknown cost/profit center mapping for sublet.", - val.line_desc, - val.part_type - ); - const partsAmount = Dinero({ - amount: Math.round((val.act_price || 0) * 100), - }) - .multiply(val.part_qty || 1) - .add( - ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || - (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && - DiscountNotAlreadyCounted(val, job.joblines) - ? val.prt_dsmk_m - ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) - : Dinero({ - amount: Math.round(val.act_price * 100), - }) - .multiply(val.part_qty || 0) - .percentage(Math.abs(val.prt_dsmk_p || 0)) - .multiply(val.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() - ); - if (!acc.sublet[partsProfitCenter]) - acc.sublet[partsProfitCenter] = Dinero(); - acc.sublet[partsProfitCenter] = - acc.sublet[partsProfitCenter].add(partsAmount); - } - - //To deal with additional costs. - if (!val.part_type && !val.mod_lbr_ty) { - //Does it already have a defined profit center? - //If so, use it, otherwise try to use the same from the auto-allocate logic in IO app jobs-close-auto-allocate. - const partsProfitCenter = - val.profitcenter_part || - getAdditionalCostCenter(val, defaultProfits) || - "Unknown"; - - if (partsProfitCenter === "Unknown") { - console.log("Unknown type", val.line_desc, val.part_type); - } - const partsAmount = Dinero({ - amount: Math.round((val.act_price || 0) * 100), - }) - .multiply(val.part_qty || 1) - .add( - ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || - (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && - DiscountNotAlreadyCounted(val, job.joblines) - ? val.prt_dsmk_m - ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) - : Dinero({ - amount: Math.round(val.act_price * 100), - }) - .multiply(val.part_qty || 0) - .percentage(Math.abs(val.prt_dsmk_p || 0)) - .multiply(val.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() - ); - - if (!acc.additional[partsProfitCenter]) - acc.additional[partsProfitCenter] = Dinero(); - acc.additional[partsProfitCenter] = - acc.additional[partsProfitCenter].add(partsAmount); - } - - return acc; - }, - { parts: {}, labor: {}, additional: {}, sublet: {} } - ); - - if (!hasMapaLine) { - if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]]) - jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = Dinero(); - jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = - jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]].add( - Dinero({ - amount: Math.round((job.rate_mapa || 0) * 100), - }).multiply(materialsHours.mapaHrs || 0) - ); - } - if (!hasMashLine) { - if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]]) - jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = Dinero(); - jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = - jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]].add( - Dinero({ - amount: Math.round((job.rate_mash || 0) * 100), - }).multiply(materialsHours.mashHrs || 0) - ); - } - - //Is it a DMS Setup? - const selectedDmsAllocationConfig = - (job.bodyshop.md_responsibility_centers.dms_defaults && - job.bodyshop.md_responsibility_centers.dms_defaults.find( - (d) => d.name === job.dms_allocation - )) || - job.bodyshop.md_responsibility_centers.defaults; - - const billTotalsByCostCenters = job.bills.reduce( - (bill_acc, bill_val) => { - //At the bill level. - bill_val.billlines.map((line_val) => { - //At the bill line level. - if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { - if ( - !bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] - ) - bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = - Dinero(); - - bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = - bill_acc[ - selectedDmsAllocationConfig.costs[line_val.cost_center] - ].add( - Dinero({ - amount: Math.round((line_val.actual_cost || 0) * 100), - }) - .multiply(line_val.quantity) - .multiply(bill_val.is_credit_memo ? -1 : 1) - ); - } else { - const isSubletCostCenter = - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.PAS || - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.PASL; - - const isAdditionalCostCenter = - // line_val.cost_center === - // job.bodyshop.md_responsibility_centers.defaults.costs.PAS || - // line_val.cost_center === - // job.bodyshop.md_responsibility_centers.defaults.costs.PASL || - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.TOW || - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA || - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.MASH; - - if (isAdditionalCostCenter) { - if (!bill_acc.additionalCosts[line_val.cost_center]) - bill_acc.additionalCosts[line_val.cost_center] = Dinero(); - - bill_acc.additionalCosts[line_val.cost_center] = - bill_acc.additionalCosts[line_val.cost_center].add( - Dinero({ - amount: Math.round((line_val.actual_cost || 0) * 100), - }) - .multiply(line_val.quantity) - .multiply(bill_val.is_credit_memo ? -1 : 1) - ); - } else if (isSubletCostCenter) { - if (!bill_acc.subletCosts[line_val.cost_center]) - bill_acc.subletCosts[line_val.cost_center] = Dinero(); - - bill_acc.subletCosts[line_val.cost_center] = bill_acc.subletCosts[ - line_val.cost_center - ].add( - Dinero({ - amount: Math.round((line_val.actual_cost || 0) * 100), - }) - .multiply(line_val.quantity) - .multiply(bill_val.is_credit_memo ? -1 : 1) - ); - } else { - if (!bill_acc[line_val.cost_center]) - bill_acc[line_val.cost_center] = Dinero(); - - bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add( - Dinero({ - amount: Math.round((line_val.actual_cost || 0) * 100), - }) - .multiply(line_val.quantity) - .multiply(bill_val.is_credit_memo ? -1 : 1) - ); - } - } - - return null; - }); - return bill_acc; - }, - { additionalCosts: {}, subletCosts: {} } - ); - - //If the hourly rates for job costing are set, add them in. - - if ( - job.bodyshop.jc_hourly_rates && - (job.bodyshop.jc_hourly_rates.mapa || - typeof job.bodyshop.jc_hourly_rates.mapa === "number" || - isNaN(job.bodyshop.jc_hourly_rates.mapa) === false) - ) { - if ( - !billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] - ) - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] = Dinero(); - if (job.bodyshop.use_paint_scale_data === true) { - if (job.mixdata.length > 0) { - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] = Dinero({ - amount: Math.round( - ((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100 - ), + //Push adjustments to bottom line. + if (job.adjustment_bottom_line) { + //Add to totals. + const Adjustment = Dinero({ + amount: Math.round(job.adjustment_bottom_line * 100), + }); //Need to invert, since this is being assigned as a cost. + summaryData.totalLaborSales = summaryData.totalLaborSales.add(Adjustment); + summaryData.totalSales = summaryData.totalSales.add(Adjustment); + //Add to lines. + costCenterData.push({ + id: "Adj", + cost_center: "Adjustment", + sale_labor: Adjustment.toFormat(), + sale_labor_dinero: Adjustment, + sale_parts: Dinero().toFormat(), + sale_parts_dinero: Dinero(), + sale_additional: Dinero(), + sale_additional_dinero: Dinero(), + 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), }); - } else { - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] = billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ].add( - Dinero({ - amount: Math.round( - (job.bodyshop.jc_hourly_rates && - job.bodyshop.jc_hourly_rates.mapa * 100) || - 0 - ), - }).multiply(materialsHours.mapaHrs) - ); - } - } else { - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] = billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ].add( - Dinero({ - amount: Math.round( - (job.bodyshop.jc_hourly_rates && - job.bodyshop.jc_hourly_rates.mapa * 100) || - 0 - ), - }).multiply(materialsHours.mapaHrs) - ); } - } - if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) { - if ( - !billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MASH - ] - ) - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MASH - ] = Dinero(); - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MASH - ] = billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MASH - ].add( - Dinero({ - amount: Math.round( - (job.bodyshop.jc_hourly_rates && - job.bodyshop.jc_hourly_rates.mash * 100) || - 0 - ), - }).multiply(materialsHours.mashHrs) + //Final summary data massaging. + + summaryData.totalLaborGp = summaryData.totalLaborSales.subtract( + summaryData.totalLaborCost + ); + summaryData.totalLaborGppercent = ( + (summaryData.totalLaborGp.getAmount() / + summaryData.totalLaborSales.getAmount()) * + 100 + ).toFixed(1); + summaryData.totalLaborGppercentFormatted = formatGpPercent( + summaryData.totalLaborGppercent ); - } - const ticketTotalsByCostCenter = job.timetickets.reduce( - (ticket_acc, ticket_val) => { - //At the invoice level. + summaryData.totalPartsGp = summaryData.totalPartsSales.subtract( + summaryData.totalPartsCost + ); + summaryData.totalPartsGppercent = ( + (summaryData.totalPartsGp.getAmount() / + summaryData.totalPartsSales.getAmount()) * + 100 + ).toFixed(1); + summaryData.totalPartsGppercentFormatted = formatGpPercent( + summaryData.totalPartsGppercent + ); + summaryData.totalAdditionalGp = summaryData.totalAdditionalSales.subtract( + summaryData.totalAdditionalCost + ); + summaryData.totalAdditionalGppercent = ( + (summaryData.totalAdditionalGp.getAmount() / + summaryData.totalAdditionalSales.getAmount()) * + 100 + ).toFixed(1); + summaryData.totalAdditionalGppercentFormatted = formatGpPercent( + summaryData.totalAdditionalGppercent + ); + summaryData.totalSubletGp = summaryData.totalSubletSales.subtract( + summaryData.totalSubletCost + ); + summaryData.totalSubletGppercent = ( + (summaryData.totalSubletGp.getAmount() / + summaryData.totalSubletSales.getAmount()) * + 100 + ).toFixed(1); + summaryData.totalSubletGppercentFormatted = formatGpPercent( + summaryData.totalSubletGppercent + ); - if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { - if ( - !ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] - ) - ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = - Dinero(); - - ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = - ticket_acc[ - selectedDmsAllocationConfig.costs[ticket_val.ciecacode] - ].add( - Dinero({ - amount: Math.round((ticket_val.rate || 0) * 100), - }).multiply( - ticket_val.flat_rate - ? ticket_val.productivehrs || ticket_val.actualhrs || 0 - : ticket_val.actualhrs || ticket_val.productivehrs || 0 - ) //Should base this on the employee. - ); - } else { - if (!ticket_acc[ticket_val.cost_center]) - ticket_acc[ticket_val.cost_center] = Dinero(); - - ticket_acc[ticket_val.cost_center] = ticket_acc[ - ticket_val.cost_center - ].add( - Dinero({ - amount: Math.round((ticket_val.rate || 0) * 100), - }).multiply( - ticket_val.flat_rate - ? ticket_val.productivehrs || ticket_val.actualhrs || 0 - : ticket_val.actualhrs || ticket_val.productivehrs || 0 - ) //Should base this on the employee. - ); - } - - return ticket_acc; - }, - {} - ); - - const summaryData = { - totalLaborSales: Dinero({ amount: 0 }), - totalPartsSales: Dinero({ amount: 0 }), - totalAdditionalSales: Dinero({ amount: 0 }), - totalSubletSales: Dinero({ amount: 0 }), - totalSales: Dinero({ amount: 0 }), - totalLaborCost: Dinero({ amount: 0 }), - totalPartsCost: Dinero({ amount: 0 }), - totalAdditionalCost: Dinero({ amount: 0 }), - totalSubletCost: Dinero({ amount: 0 }), - totalCost: Dinero({ amount: 0 }), - totalLaborGp: Dinero({ amount: 0 }), - totalPartsGp: Dinero({ amount: 0 }), - totalAdditionalGp: Dinero({ amount: 0 }), - totalSubletGp: Dinero({ amount: 0 }), - gpdollars: Dinero({ amount: 0 }), - totalLaborGppercent: null, - totalLaborGppercentFormatted: null, - totalPartsGppercent: null, - totalPartsGppercentFormatted: null, - totalAdditionalGppercent: null, - totalAdditionalGppercentFormatted: null, - totalSubletGppercent: null, - totalSubletGppercentFormatted: null, - gppercent: null, - gppercentFormatted: null, - }; - - const costCenterData = allCenters.map((key, idx) => { - const ccVal = key; // defaultProfits[key]; - const sale_labor = - jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({ amount: 0 }); - const sale_parts = - jobLineTotalsByProfitCenter.parts[ccVal] || Dinero({ amount: 0 }); - const sale_additional = - jobLineTotalsByProfitCenter.additional[ccVal] || Dinero({ amount: 0 }); - const sale_sublet = - jobLineTotalsByProfitCenter.sublet[ccVal] || Dinero({ amount: 0 }); - - const cost_labor = ticketTotalsByCostCenter[ccVal] || Dinero({ amount: 0 }); - const cost_parts = billTotalsByCostCenters[ccVal] || Dinero({ amount: 0 }); - const cost_additional = - billTotalsByCostCenters.additionalCosts[ccVal] || Dinero({ amount: 0 }); - const cost_sublet = - billTotalsByCostCenters.subletCosts[ccVal] || Dinero({ amount: 0 }); - - const costs = cost_labor - .add(cost_parts) - .add(cost_additional) - .add(cost_sublet); - const totalSales = sale_labor - .add(sale_parts) - .add(sale_additional) - .add(sale_sublet); - const gpdollars = totalSales.subtract(costs); - const gppercent = ( - (gpdollars.getAmount() / Math.abs(totalSales.getAmount())) * - 100 + summaryData.gpdollars = summaryData.totalSales.subtract( + summaryData.totalCost + ); + summaryData.gppercent = ( + (summaryData.gpdollars.getAmount() / + Math.abs(summaryData.totalSales.getAmount())) * + 100 ).toFixed(1); - //Push summary data to avoid extra loop. - summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor); - summaryData.totalPartsSales = summaryData.totalPartsSales.add(sale_parts); - summaryData.totalAdditionalSales = - summaryData.totalAdditionalSales.add(sale_additional); - summaryData.totalSubletSales = - summaryData.totalSubletSales.add(sale_sublet); - summaryData.totalSales = summaryData.totalSales.add(totalSales); - summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor); - summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts); - summaryData.totalAdditionalCost = - summaryData.totalAdditionalCost.add(cost_additional); - summaryData.totalSubletCost = summaryData.totalSubletCost.add(cost_sublet); - summaryData.totalCost = summaryData.totalCost.add(costs); + if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0; + else if (!isFinite(summaryData.gppercent)) + summaryData.gppercentFormatted = "- ∞"; + else { + summaryData.gppercentFormatted = `${summaryData.gppercent}%`; + } - return { - id: idx, - cost_center: ccVal, - sale_labor: sale_labor && sale_labor.toFormat(), - sale_labor_dinero: sale_labor, - sale_parts: sale_parts && sale_parts.toFormat(), - sale_parts_dinero: sale_parts, - sale_additional: sale_additional && sale_additional.toFormat(), - sale_additional_dinero: sale_additional, - sale_sublet: sale_sublet && sale_sublet.toFormat(), - sale_sublet_dinero: sale_sublet, - sales: totalSales.toFormat(), - sales_dinero: totalSales, - cost_parts: cost_parts && cost_parts.toFormat(), - cost_parts_dinero: cost_parts, - cost_labor: cost_labor && cost_labor.toFormat(), - cost_labor_dinero: cost_labor, - cost_additional: cost_additional && cost_additional.toFormat(), - cost_additional_dinero: cost_additional, - cost_sublet: cost_sublet && cost_sublet.toFormat(), - cost_sublet_dinero: cost_sublet, - costs: costs.toFormat(), - costs_dinero: costs, - gpdollars_dinero: gpdollars, - gpdollars: gpdollars.toFormat(), - gppercent: formatGpPercent(gppercent), - }; - }); - - //Push adjustments to bottom line. - if (job.adjustment_bottom_line) { - //Add to totals. - const Adjustment = Dinero({ - amount: Math.round(job.adjustment_bottom_line * 100), - }); //Need to invert, since this is being assigned as a cost. - summaryData.totalLaborSales = summaryData.totalLaborSales.add(Adjustment); - summaryData.totalSales = summaryData.totalSales.add(Adjustment); - //Add to lines. - costCenterData.push({ - id: "Adj", - cost_center: "Adjustment", - sale_labor: Adjustment.toFormat(), - sale_labor_dinero: Adjustment, - sale_parts: Dinero().toFormat(), - sale_parts_dinero: Dinero(), - sale_additional: Dinero(), - sale_additional_dinero: Dinero(), - 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. - - summaryData.totalLaborGp = summaryData.totalLaborSales.subtract( - summaryData.totalLaborCost - ); - summaryData.totalLaborGppercent = ( - (summaryData.totalLaborGp.getAmount() / - summaryData.totalLaborSales.getAmount()) * - 100 - ).toFixed(1); - summaryData.totalLaborGppercentFormatted = formatGpPercent( - summaryData.totalLaborGppercent - ); - - summaryData.totalPartsGp = summaryData.totalPartsSales.subtract( - summaryData.totalPartsCost - ); - summaryData.totalPartsGppercent = ( - (summaryData.totalPartsGp.getAmount() / - summaryData.totalPartsSales.getAmount()) * - 100 - ).toFixed(1); - summaryData.totalPartsGppercentFormatted = formatGpPercent( - summaryData.totalPartsGppercent - ); - summaryData.totalAdditionalGp = summaryData.totalAdditionalSales.subtract( - summaryData.totalAdditionalCost - ); - summaryData.totalAdditionalGppercent = ( - (summaryData.totalAdditionalGp.getAmount() / - summaryData.totalAdditionalSales.getAmount()) * - 100 - ).toFixed(1); - summaryData.totalAdditionalGppercentFormatted = formatGpPercent( - summaryData.totalAdditionalGppercent - ); - summaryData.totalSubletGp = summaryData.totalSubletSales.subtract( - summaryData.totalSubletCost - ); - summaryData.totalSubletGppercent = ( - (summaryData.totalSubletGp.getAmount() / - summaryData.totalSubletSales.getAmount()) * - 100 - ).toFixed(1); - summaryData.totalSubletGppercentFormatted = formatGpPercent( - summaryData.totalSubletGppercent - ); - - summaryData.gpdollars = summaryData.totalSales.subtract( - summaryData.totalCost - ); - summaryData.gppercent = ( - (summaryData.gpdollars.getAmount() / - Math.abs(summaryData.totalSales.getAmount())) * - 100 - ).toFixed(1); - - if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0; - else if (!isFinite(summaryData.gppercent)) - summaryData.gppercentFormatted = "- ∞"; - else { - summaryData.gppercentFormatted = `${summaryData.gppercent}%`; - } - - return { summaryData, costCenterData }; + return {summaryData, costCenterData}; } exports.JobCosting = JobCosting; exports.JobCostingMulti = JobCostingMulti; const formatGpPercent = (gppercent) => { - let gppercentFormatted; - if (isNaN(gppercent)) gppercentFormatted = "0%"; - else if (!isFinite(gppercent)) gppercentFormatted = "- ∞"; - else { - gppercentFormatted = `${gppercent}%`; - } + let gppercentFormatted; + if (isNaN(gppercent)) gppercentFormatted = "0%"; + else if (!isFinite(gppercent)) gppercentFormatted = "- ∞"; + else { + gppercentFormatted = `${gppercent}%`; + } - return gppercentFormatted; + return gppercentFormatted; }; //Verify that this stays in line with jobs-close-auto-allocate logic from the application. const getAdditionalCostCenter = (jl, profitCenters) => { - if (!jl.part_type && !jl.mod_lbr_ty) { - const lineDesc = jl.line_desc ? jl.line_desc.toLowerCase() : ""; + if (!jl.part_type && !jl.mod_lbr_ty) { + const lineDesc = jl.line_desc ? jl.line_desc.toLowerCase() : ""; - if (lineDesc.includes("shop mat")) { - return profitCenters["MASH"]; - } else if (lineDesc.includes("paint/mat")) { - return profitCenters["MAPA"]; - } else if (lineDesc.includes("ats amount")) { - return profitCenters["ATS"]; - } else if (lineDesc.includes("towing")) { - return profitCenters["TOW"]; - } else { - return null; + if (lineDesc.includes("shop mat")) { + return profitCenters["MASH"]; + } else if (lineDesc.includes("paint/mat")) { + return profitCenters["MAPA"]; + } else if (lineDesc.includes("ats amount")) { + return profitCenters["ATS"]; + } else if (lineDesc.includes("towing")) { + return profitCenters["TOW"]; + } else { + return null; + } } - } }; diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 61e3703fb..25affd6cf 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,6 +1,7 @@ const _ = require("lodash"); const jobLifecycle = (req, res) => { const {jobids} = req.body; + return _.isArray(jobids) ? handleMultipleJobs(jobids, req, res) : handleSingleJob(jobids, req, res); @@ -11,6 +12,9 @@ const handleMultipleJobs = (jobIDs, req, res) => { } const handleSingleJob = (req, res) => { + + const client = req.userGraphQLClient; + return res.status(200).send(req.body); } diff --git a/server/job/job-totals.js b/server/job/job-totals.js index 35246ec8c..b942871cf 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -1,20 +1,18 @@ const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); -const GraphQLClient = require("graphql-request").GraphQLClient; -const logger = require("../utils/logger"); +const logger = require('../utils/logger'); + // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; Dinero.globalRoundingMode = "HALF_EVEN"; exports.totalsSsu = async function (req, res) { - const BearerToken = req.headers.authorization; const { id } = req.body; + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("job-totals-ssu", "DEBUG", req.user.email, id, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); try { const job = await client @@ -75,21 +73,19 @@ async function TotalsServerSide(req, res) { } async function Totals(req, res) { - const { job } = req.body; + const { job, id } = req.body; + + const logger = req.logger; + const client = req.userGraphQLClient; + logger.log("job-totals", "DEBUG", req.user.email, job.id, { jobid: job.id, }); - const BearerToken = req.headers.authorization; - const { id } = req.body; logger.log("job-totals-ssu", "DEBUG", req.user.email, id, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); await AutoAddAtsIfRequired({ job, client }); + try { let ret = { parts: CalculatePartsTotals(job.joblines), diff --git a/server/middleware/withUserGraphQLClientMiddleware.js b/server/middleware/withUserGraphQLClientMiddleware.js new file mode 100644 index 000000000..e55b58c8d --- /dev/null +++ b/server/middleware/withUserGraphQLClientMiddleware.js @@ -0,0 +1,24 @@ +const {GraphQLClient} = require("graphql-request"); + +/** + * Middleware to add a GraphQL Client to the request object + * Adds the following to the request object: + * req.userGraphQLClient - GraphQL Client with user Bearer Token + * req.BearerToken - Bearer Token + * @param req + * @param res + * @param next + */ +const withUserGraphQLClientMiddleware = (req, res, next) => { + const BearerToken = req.headers.authorization; + req.userGraphQLClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { + headers: { + Authorization: BearerToken, + }, + }); + req.BearerToken = BearerToken; + + next(); +}; + +module.exports = withUserGraphQLClientMiddleware; \ No newline at end of file diff --git a/server/mixdata/mixdata.js b/server/mixdata/mixdata.js index a0d5141f3..41a3b0eb8 100644 --- a/server/mixdata/mixdata.js +++ b/server/mixdata/mixdata.js @@ -1,9 +1,8 @@ const path = require("path"); const _ = require("lodash"); -const logger = require("../utils/logger"); const xml2js = require("xml2js"); -const GraphQLClient = require("graphql-request").GraphQLClient; const queries = require("../graphql-client/queries"); +const logger = require('../utils/logger'); require("dotenv").config({ path: path.resolve( @@ -15,13 +14,10 @@ require("dotenv").config({ exports.mixdataUpload = async (req, res) => { const { bodyshopid } = req.body; - const BearerToken = req.headers.authorization; + const client = req.userGraphQLClient; + logger.log("job-mixdata-upload", "DEBUG", req.user.email, null, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + try { for (const element of req.files) { diff --git a/server/opensearch/os-handler.js b/server/opensearch/os-handler.js index 2beeaca3a..5fe63695c 100644 --- a/server/opensearch/os-handler.js +++ b/server/opensearch/os-handler.js @@ -5,7 +5,6 @@ require("dotenv").config({ ), }); -const GraphQLClient = require("graphql-request").GraphQLClient; //const client = require("../graphql-client/graphql-client").client; const logger = require("../utils/logger"); const queries = require("../graphql-client/queries"); @@ -182,12 +181,8 @@ async function OpenSearchSearchHandler(req, res) { search, }); - const BearerToken = req.headers.authorization; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; const assocs = await client .setHeaders({Authorization: BearerToken}) diff --git a/server/parts-scan/parts-scan.js b/server/parts-scan/parts-scan.js index c5b619303..b2f51ef3b 100644 --- a/server/parts-scan/parts-scan.js +++ b/server/parts-scan/parts-scan.js @@ -1,21 +1,19 @@ const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); +const logger = require('../utils/logger'); const { job } = require("../scheduling/scheduling-job"); -const GraphQLClient = require("graphql-request").GraphQLClient; -const logger = require("../utils/logger"); const _ = require("lodash"); + // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; exports.partsScan = async function (req, res) { - const BearerToken = req.headers.authorization; const { jobid } = req.body; + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("job-parts-scan", "DEBUG", req.user?.email, jobid, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); try { //Query all jobline data using the user's authorization. diff --git a/server/routes/accountingRoutes.js b/server/routes/accountingRoutes.js index aff49cbdb..04576bf01 100644 --- a/server/routes/accountingRoutes.js +++ b/server/routes/accountingRoutes.js @@ -2,11 +2,12 @@ const express = require('express'); const router = express.Router(); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const {payments, payables, receivables} = require("../accounting/qbxml/qbxml"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); router.use(validateFirebaseIdTokenMiddleware); -router.post('/qbxml/receivables', receivables); -router.post('/qbxml/payables', payables); -router.post('/qbxml/payments', payments); +router.post('/qbxml/receivables', withUserGraphQLClientMiddleware, receivables); +router.post('/qbxml/payables', withUserGraphQLClientMiddleware, payables); +router.post('/qbxml/payments', withUserGraphQLClientMiddleware, payments); module.exports = router; diff --git a/server/routes/cdkRoutes.js b/server/routes/cdkRoutes.js index fdedadf92..85d2b49d0 100644 --- a/server/routes/cdkRoutes.js +++ b/server/routes/cdkRoutes.js @@ -2,9 +2,10 @@ const express = require('express'); const router = express.Router(); const cdkGetMake = require('../cdk/cdk-get-makes'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); router.use(validateFirebaseIdTokenMiddleware); -router.post('/getvehicles', cdkGetMake.default); +router.post('/getvehicles', withUserGraphQLClientMiddleware, cdkGetMake.default); module.exports = router; diff --git a/server/routes/jobRoutes.js b/server/routes/jobRoutes.js index dea660200..e11187041 100644 --- a/server/routes/jobRoutes.js +++ b/server/routes/jobRoutes.js @@ -5,15 +5,16 @@ const {partsScan} = require('../parts-scan/parts-scan'); const eventAuthorizationMiddleware = require('../middleware/eventAuthorizationMIddleware'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); const {totals, statustransition, totalsSsu, costing, lifecycle, costingmulti} = require("../job/job"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); router.use(validateFirebaseIdTokenMiddleware); -router.post('/totals', totals); +router.post('/totals', withUserGraphQLClientMiddleware, totals); router.post('/statustransition', eventAuthorizationMiddleware, statustransition); -router.post('/totalsssu', totalsSsu); -router.post('/costing', costing); -router.get('/lifecycle', lifecycle); -router.post('/costingmulti', costingmulti); -router.post('/partsscan', partsScan); +router.post('/totalsssu', withUserGraphQLClientMiddleware,totalsSsu); +router.post('/costing', withUserGraphQLClientMiddleware,costing); +router.get('/lifecycle', withUserGraphQLClientMiddleware, lifecycle); +router.post('/costingmulti', withUserGraphQLClientMiddleware, costingmulti); +router.post('/partsscan', withUserGraphQLClientMiddleware, partsScan); module.exports = router; diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js index fcdc23398..9d20d9b42 100644 --- a/server/routes/miscellaneousRoutes.js +++ b/server/routes/miscellaneousRoutes.js @@ -9,6 +9,7 @@ const taskHandler = require("../tasks/tasks"); const os = require("../opensearch/os-handler"); const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware"); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); //Test route to ensure Express is responding. router.get("/test", async function (req, res) { @@ -18,7 +19,7 @@ router.get("/test", async function (req, res) { // console.log(app.get('trust proxy')); // console.log("remoteAddress", req.socket.remoteAddress); // console.log("X-Forwarded-For", req.header('x-forwarded-for')); - logger.log("test-api-status", "DEBUG", "api", { commit }); + logger.log("test-api-status", "DEBUG", "api", {commit}); // sendEmail.sendServerEmail({ // subject: `API Check - ${process.env.NODE_ENV}`, // text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`, @@ -31,7 +32,7 @@ router.get("/test", async function (req, res) { }); // Search -router.post("/search", validateFirebaseIdTokenMiddleware, os.search); +router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search); router.post("/opensearch", eventAuthorizationMiddleware, os.handler); diff --git a/server/routes/mixDataRoutes.js b/server/routes/mixDataRoutes.js index f3f4d8afe..b9ac289e7 100644 --- a/server/routes/mixDataRoutes.js +++ b/server/routes/mixDataRoutes.js @@ -4,7 +4,8 @@ const multer = require('multer'); const upload = multer(); const {mixdataUpload} = require('../mixdata/mixdata'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); -router.post('/upload', validateFirebaseIdTokenMiddleware, upload.any(), mixdataUpload); +router.post('/upload', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, upload.any(), mixdataUpload); module.exports = router; diff --git a/server/routes/qboRoutes.js b/server/routes/qboRoutes.js index e7a00619f..22b54e23e 100644 --- a/server/routes/qboRoutes.js +++ b/server/routes/qboRoutes.js @@ -1,13 +1,14 @@ const express = require('express'); const router = express.Router(); const {authorize, callback, receivables, payables, payments} = require('../accounting/qbo/qbo'); -const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); // Assuming you have a qbo module for handling QuickBooks Online related functionalities +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); // Assuming you have a qbo module for handling QuickBooks Online related functionalities // Define the routes for QuickBooks Online router.post('/authorize', validateFirebaseIdTokenMiddleware, authorize); router.get('/callback', callback); -router.post('/receivables', validateFirebaseIdTokenMiddleware, receivables); -router.post('/payables', validateFirebaseIdTokenMiddleware, payables); -router.post('/payments', validateFirebaseIdTokenMiddleware, payments); +router.post('/receivables', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, receivables); +router.post('/payables', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, payables); +router.post('/payments', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, payments); module.exports = router; diff --git a/server/routes/schedulingRoutes.js b/server/routes/schedulingRoutes.js index 38a91229b..816114315 100644 --- a/server/routes/schedulingRoutes.js +++ b/server/routes/schedulingRoutes.js @@ -2,7 +2,8 @@ const express = require('express'); const router = express.Router(); const {job} = require('../scheduling/scheduling-job'); const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); -router.post('/job', validateFirebaseIdTokenMiddleware, job); +router.post('/job', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, job); module.exports = router; diff --git a/server/scheduling/scheduling-job.js b/server/scheduling/scheduling-job.js index e906c866e..178ef3c73 100644 --- a/server/scheduling/scheduling-job.js +++ b/server/scheduling/scheduling-job.js @@ -1,4 +1,3 @@ -const GraphQLClient = require("graphql-request").GraphQLClient; const path = require("path"); const queries = require("../graphql-client/queries"); const Dinero = require("dinero.js"); @@ -14,17 +13,14 @@ require("dotenv").config({ }); exports.job = async (req, res) => { - const BearerToken = req.headers.authorization; const { jobId } = req.body; + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + try { logger.log("smart-scheduling-start", "DEBUG", req.user.email, jobId, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); - const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_UPCOMING_APPOINTMENTS, { From 09d112350a3fc23ff386e0a2ef8eee8d9688aebf Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 01:37:11 -0500 Subject: [PATCH 15/48] - Rough in front end / backend Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 77 +++++++++++++++++++ .../jobs-detail.page.component.jsx | 8 ++ server/graphql-client/queries.js | 14 ++++ server/job/job-lifecycle.js | 13 +++- .../eventAuthorizationMIddleware.js | 8 ++ server/routes/jobRoutes.js | 14 ++-- 6 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 client/src/components/job-lifecycle/job-lifecycle.component.jsx diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx new file mode 100644 index 000000000..dc6925055 --- /dev/null +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -0,0 +1,77 @@ +import {createStructuredSelector} from "reselect"; +import {selectBodyshop} from "../../redux/user/user.selectors"; +import {connect} from "react-redux"; +import {useEffect, useState} from "react"; +import axios from "axios"; +import {Card, Space, Table, Timeline} from "antd"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export function JobLifecycleComponent({bodyshop, job, ...rest}) { + const [loading, setLoading] = useState(false); + const [lifecycleData, setLifecycleData] = useState(null); + + + useEffect(() => { + async function getLifecycleData() { + if (job && job.id) { + setLoading(true); + const response = await axios.post("/job/lifecycle", { + jobids: job.id, + }); + console.dir(response.data.data.transitions, {depth: null}); + setLifecycleData(response.data.data.transitions); + setLoading(false); + } + } + + getLifecycleData().catch((err) => { + console.log(`Something went wrong getting Job Lifecycle Data: ${err.message}`); + setLoading(false); + }); + }, [job]); + + const columnKeys = [ + 'start', + 'end', + 'value', + 'prev_value', + 'next_value', + 'duration', + 'type', + 'created_at', + 'updated_at' + ]; + + const columns = columnKeys.map(key => ({ + title: key.charAt(0).toUpperCase() + key.slice(1), // Capitalize the first letter for the title + dataIndex: key, + key: key, + })); + + return ( + + + + + + + + {lifecycleData.map((item, index) => ( + + {item.value} - {new Date(item.start).toLocaleString()} + + ))} + + + + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent); diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index 001a18166..a7d51d5e3 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -54,6 +54,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import UndefinedToNull from "../../utils/undefinedtonull"; import { DateTimeFormat } from "./../../utils/DateFormatter"; +import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -288,6 +289,13 @@ export function JobsDetailPage({ form={form} /> + Lifecycle} + key="lifecycle" + > + + { const {jobids} = req.body; @@ -11,11 +12,17 @@ const handleMultipleJobs = (jobIDs, req, res) => { return res.status(200).send(jobIDs); } -const handleSingleJob = (req, res) => { - +const handleSingleJob = async (jobIds, req, res) => { const client = req.userGraphQLClient; - return res.status(200).send(req.body); + const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobid: jobIds,}); + + const response = { + jobIds, + data: resp + } + + return res.status(200).json(response); } module.exports = jobLifecycle; \ No newline at end of file diff --git a/server/middleware/eventAuthorizationMIddleware.js b/server/middleware/eventAuthorizationMIddleware.js index c766ceda5..423fbc73f 100644 --- a/server/middleware/eventAuthorizationMIddleware.js +++ b/server/middleware/eventAuthorizationMIddleware.js @@ -1,3 +1,11 @@ +const path = require("path"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); + /** * Checks if the event secret is correct * It adds the following properties to the request object: diff --git a/server/routes/jobRoutes.js b/server/routes/jobRoutes.js index e11187041..9b4a5e9f6 100644 --- a/server/routes/jobRoutes.js +++ b/server/routes/jobRoutes.js @@ -7,14 +7,12 @@ const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebas const {totals, statustransition, totalsSsu, costing, lifecycle, costingmulti} = require("../job/job"); const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); -router.use(validateFirebaseIdTokenMiddleware); - -router.post('/totals', withUserGraphQLClientMiddleware, totals); +router.post('/totals', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals); router.post('/statustransition', eventAuthorizationMiddleware, statustransition); -router.post('/totalsssu', withUserGraphQLClientMiddleware,totalsSsu); -router.post('/costing', withUserGraphQLClientMiddleware,costing); -router.get('/lifecycle', withUserGraphQLClientMiddleware, lifecycle); -router.post('/costingmulti', withUserGraphQLClientMiddleware, costingmulti); -router.post('/partsscan', withUserGraphQLClientMiddleware, partsScan); +router.post('/totalsssu', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware,totalsSsu); +router.post('/costing', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware,costing); +router.post('/lifecycle', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, lifecycle); +router.post('/costingmulti', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti); +router.post('/partsscan', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan); module.exports = router; From cfe072744711fb98826e1b43ce70383e62278586 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 10:20:26 -0500 Subject: [PATCH 16/48] - Rough in front end / backend Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 34 +++++++++++-------- .../eventAuthorizationMIddleware.js | 6 ---- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index dc6925055..c9303177e 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -13,7 +13,7 @@ const mapDispatchToProps = (dispatch) => ({ }); export function JobLifecycleComponent({bodyshop, job, ...rest}) { - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); @@ -56,20 +56,26 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { return ( - - -
+ {!loading ? ( + + +
+ + + + {lifecycleData.map((item, index) => ( + + {item.value} - {new Date(item.start).toLocaleString()} + + ))} + + + + ) : ( + + Loading Job Timelines.... - - - {lifecycleData.map((item, index) => ( - - {item.value} - {new Date(item.start).toLocaleString()} - - ))} - - - + )} ); } diff --git a/server/middleware/eventAuthorizationMIddleware.js b/server/middleware/eventAuthorizationMIddleware.js index 423fbc73f..9dd4dfd3a 100644 --- a/server/middleware/eventAuthorizationMIddleware.js +++ b/server/middleware/eventAuthorizationMIddleware.js @@ -1,10 +1,4 @@ const path = require("path"); -require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), -}); /** * Checks if the event secret is correct From f59bdf90303fb6a931c7fa6d16424d6a2da682df Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 12:35:58 -0500 Subject: [PATCH 17/48] - Progress Update Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 100 +++++++++++++++--- server/graphql-client/queries.js | 5 +- server/job/job-lifecycle.js | 75 ++++++++++--- 3 files changed, 147 insertions(+), 33 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index c9303177e..3d356c575 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,9 +1,10 @@ import {createStructuredSelector} from "reselect"; import {selectBodyshop} from "../../redux/user/user.selectors"; import {connect} from "react-redux"; -import {useEffect, useState} from "react"; +import {useCallback, useEffect, useState} from "react"; import axios from "axios"; import {Card, Space, Table, Timeline} from "antd"; +import {Cell, LabelList, Legend, Pie, PieChart, Tooltip} from "recharts"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -12,6 +13,9 @@ const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); +const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']; + + export function JobLifecycleComponent({bodyshop, job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); @@ -24,8 +28,8 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { const response = await axios.post("/job/lifecycle", { jobids: job.id, }); - console.dir(response.data.data.transitions, {depth: null}); - setLifecycleData(response.data.data.transitions); + const data = response.data.transition[job.id]; + setLifecycleData(data); setLoading(false); } } @@ -36,6 +40,11 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { }); }, [job]); + // // TODO - Delete this useEffect, it is for testing + // useEffect(() => { + // console.dir(lifecycleData) + // }, [lifecycleData]); + const columnKeys = [ 'start', 'end', @@ -54,23 +63,82 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { key: key, })); + /** + * Returns an array of cells for the Pie Chart + * @type {function(): *[]} + */ + const renderCells = useCallback(() => { + const entires = Object + .entries(lifecycleData.durations) + .filter(([name, value]) => { + return value !== 0; + }) + + return entires.map(([name, value], index) => ( + + + + + )); + }, [lifecycleData, job]); + + /** + * Returns an array of objects with the name and value of the duration + * @type {function(): {name: *, value}[]} + */ + const durationsData = useCallback(() => { + return Object.entries(lifecycleData.durations) .filter(([name, value]) => { + return value !== 0; + }).map(([name, value]) => ({ + name, + value: value / 1000 + })) + }, [lifecycleData, job]); + return ( {!loading ? ( - - -
+ lifecycleData ? ( + + +
+ + + + + {lifecycleData.lifecycle.map((item, index) => ( + + {item.value} - {new Date(item.start).toLocaleString()} + + ))} + + + + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {renderCells()} + + + + + + + + + ) : ( + + There is currently no lifecycle data for this job. - - - {lifecycleData.map((item, index) => ( - - {item.value} - {new Date(item.start).toLocaleString()} - - ))} - - - + ) ) : ( Loading Job Timelines.... diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index a642a4988..782bd3100 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -518,8 +518,8 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = ` } }`; -exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobid: uuid!) { - transitions(where: {jobid: {_eq: $jobid}}, order_by: {id: asc}) { +exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) { + transitions(where: {jobid: {_in: $jobids}}, order_by: {created_at: asc}) { start end value @@ -529,6 +529,7 @@ exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobid: u type created_at updated_at + jobid } }`; diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 207a514e3..8eddd86a5 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,28 +1,73 @@ const _ = require("lodash"); const queries = require("../graphql-client/queries"); -const jobLifecycle = (req, res) => { + +const calculateStatusDuration = (transitions) => { + let statusDuration = {}; + + transitions.forEach((transition, index) => { + let duration = transition.duration; + + // If there is no prev_value, it is the first transition + if (!transition.prev_value) { + statusDuration[transition.value] = duration; + } + // If there is no next_value, it is the last transition (the active one) + else if (!transition.next_value) { + if (statusDuration[transition.value]) { + statusDuration[transition.value] += duration; + } else { + statusDuration[transition.value] = duration; + } + } + // For all other transitions + else { + if (statusDuration[transition.value]) { + statusDuration[transition.value] += duration; + } else { + statusDuration[transition.value] = duration; + } + } + }); + + return statusDuration; +} + + +const jobLifecycle = async (req, res) => { const {jobids} = req.body; - return _.isArray(jobids) ? - handleMultipleJobs(jobids, req, res) : - handleSingleJob(jobids, req, res); -}; + const jobIDs = _.isArray(jobids) ? jobids : [jobids]; -const handleMultipleJobs = (jobIDs, req, res) => { - return res.status(200).send(jobIDs); -} - -const handleSingleJob = async (jobIds, req, res) => { const client = req.userGraphQLClient; - const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobid: jobIds,}); + const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobids: jobIDs,}); + + const transitions = resp.transitions; + + if (!transitions) { + return res.status(200).json({ + jobIDs, + transitions: [] + }); - const response = { - jobIds, - data: resp } - return res.status(200).json(response); + const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); + + const groupedTransitions = {}; + + for (let jobId in transitionsByJobId) { + groupedTransitions[jobId] = { + lifecycle: transitionsByJobId[jobId], + durations: calculateStatusDuration(transitionsByJobId[jobId]) + }; + } + + return res.status(200).json({ + jobIDs, + transition: groupedTransitions, + }); } + module.exports = jobLifecycle; \ No newline at end of file From 5de4ef5d837a104ccf018b598b38fab4b87e1fe5 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 12:54:38 -0500 Subject: [PATCH 18/48] - human readable dates Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 10 +++++---- server/job/job-lifecycle.js | 22 +++++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 3d356c575..626d26545 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -54,7 +54,9 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { 'duration', 'type', 'created_at', - 'updated_at' + 'updated_at', + 'start_readable', + 'end_readable', ]; const columns = columnKeys.map(key => ({ @@ -71,7 +73,7 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { const entires = Object .entries(lifecycleData.durations) .filter(([name, value]) => { - return value !== 0; + return value > 0; }) return entires.map(([name, value], index) => ( @@ -88,7 +90,7 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { */ const durationsData = useCallback(() => { return Object.entries(lifecycleData.durations) .filter(([name, value]) => { - return value !== 0; + return value > 0; }).map(([name, value]) => ({ name, value: value / 1000 @@ -108,7 +110,7 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { {lifecycleData.lifecycle.map((item, index) => ( - {item.value} - {new Date(item.start).toLocaleString()} + {item.value} - {item.start_readable} ))} diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 8eddd86a5..bc24e9c8a 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,5 +1,6 @@ const _ = require("lodash"); const queries = require("../graphql-client/queries"); +const moment = require("moment"); const calculateStatusDuration = (transitions) => { let statusDuration = {}; @@ -52,17 +53,30 @@ const jobLifecycle = async (req, res) => { } + const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const groupedTransitions = {}; - + moment.relativeTimeThreshold('m', 30) for (let jobId in transitionsByJobId) { + let lifecycle = transitionsByJobId[jobId].map(transition => { + if (transition.start) { + transition.start_readable = moment(transition.start).fromNow(); + } + if (transition.end) { + transition.end_readable = moment(transition.end).fromNow(); + } + return transition; + }); + groupedTransitions[jobId] = { - lifecycle: transitionsByJobId[jobId], - durations: calculateStatusDuration(transitionsByJobId[jobId]) + lifecycle: lifecycle, + durations: calculateStatusDuration(lifecycle) }; } - + + console.dir(groupedTransitions, {depth: null}); + return res.status(200).json({ jobIDs, transition: groupedTransitions, From d0a2bb7da0610f2773dbb3abb8dc66966334eb17 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 12:58:57 -0500 Subject: [PATCH 19/48] - human readable dates Signed-off-by: Dave Richer --- client/src/components/job-lifecycle/job-lifecycle.component.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 626d26545..8122229af 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -15,7 +15,6 @@ const mapDispatchToProps = (dispatch) => ({ const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']; - export function JobLifecycleComponent({bodyshop, job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); From d740446ccb7ecbe7494f4e8e2f76e40c0c26d6a7 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 24 Jan 2024 10:07:07 -0500 Subject: [PATCH 20/48] - Progress Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 165 ++++++++---------- .../jobs-detail.page.component.jsx | 2 +- server/job/job-lifecycle.js | 31 +++- 3 files changed, 92 insertions(+), 106 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 8122229af..62ecb920a 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,139 +1,110 @@ -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import {connect} from "react-redux"; -import {useCallback, useEffect, useState} from "react"; -import axios from "axios"; -import {Card, Space, Table, Timeline} from "antd"; -import {Cell, LabelList, Legend, Pie, PieChart, Tooltip} from "recharts"; +import React, {useEffect, useMemo, useState} from 'react'; +import axios from 'axios'; +import {Card, Space, Table, Timeline} from 'antd'; +import {Bar, BarChart, CartesianGrid, LabelList, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; -const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, -}); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); +export function JobLifecycleComponent({job, ...rest}) { -const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']; - -export function JobLifecycleComponent({bodyshop, job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); - useEffect(() => { - async function getLifecycleData() { + const getLifecycleData = async () => { if (job && job.id) { - setLoading(true); - const response = await axios.post("/job/lifecycle", { - jobids: job.id, - }); - const data = response.data.transition[job.id]; - setLifecycleData(data); - setLoading(false); + try { + setLoading(true); + const response = await axios.post("/job/lifecycle", {jobids: job.id}); + const data = response.data.transition[job.id]; + setLifecycleData(data); + } catch (err) { + console.error(`Error getting Job Lifecycle Data: ${err.message}`); + } finally { + setLoading(false); + } } - } + }; - getLifecycleData().catch((err) => { - console.log(`Something went wrong getting Job Lifecycle Data: ${err.message}`); - setLoading(false); - }); + getLifecycleData(); }, [job]); - // // TODO - Delete this useEffect, it is for testing - // useEffect(() => { - // console.dir(lifecycleData) - // }, [lifecycleData]); - const columnKeys = [ - 'start', - 'end', - 'value', - 'prev_value', - 'next_value', - 'duration', - 'type', - 'created_at', - 'updated_at', - 'start_readable', - 'end_readable', + 'start', 'end', 'value', 'prev_value', 'next_value', 'duration', 'type', 'created_at', 'updated_at', 'start_readable', 'end_readable','duration' ]; const columns = columnKeys.map(key => ({ - title: key.charAt(0).toUpperCase() + key.slice(1), // Capitalize the first letter for the title + title: key.charAt(0).toUpperCase() + key.slice(1), dataIndex: key, key: key, })); - /** - * Returns an array of cells for the Pie Chart - * @type {function(): *[]} - */ - const renderCells = useCallback(() => { - const entires = Object - .entries(lifecycleData.durations) - .filter(([name, value]) => { - return value > 0; - }) - return entires.map(([name, value], index) => ( - - - - - )); - }, [lifecycleData, job]); + const durationsData = useMemo(() => { + if (!lifecycleData) { + return []; + } - /** - * Returns an array of objects with the name and value of the duration - * @type {function(): {name: *, value}[]} - */ - const durationsData = useCallback(() => { - return Object.entries(lifecycleData.durations) .filter(([name, value]) => { - return value > 0; - }).map(([name, value]) => ({ - name, - value: value / 1000 - })) - }, [lifecycleData, job]); + const transformedData = Object.entries(lifecycleData.durations).map(([name, {value, humanReadable}]) => { + return { + name, + amt: value, + pv: humanReadable, + uv: value, + } + }) + + return [transformedData]; + }, [lifecycleData]); + + + useEffect(() => { + console.dir(lifecycleData, {depth: null}) + console.dir(durationsData, {depth: null}) + + }, [lifecycleData,durationsData]); return ( {!loading ? ( lifecycleData ? ( - -
- {lifecycleData.lifecycle.map((item, index) => ( - + {item.value} - {item.start_readable} ))} - - `${name}: ${(percent * 100).toFixed(0)}%`} - outerRadius={80} - fill="#8884d8" - dataKey="value" + + - {renderCells()} - - - - + + + + + + + + + - + +
+ ) : ( @@ -149,4 +120,4 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { ); } -export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent); +export default JobLifecycleComponent; \ No newline at end of file diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index a7d51d5e3..0f335d982 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -294,7 +294,7 @@ export function JobsDetailPage({ tab={Lifecycle} key="lifecycle" > - + { let statusDuration = {}; transitions.forEach((transition, index) => { - let duration = transition.duration; + let duration = transition.duration_minutes; // If there is no prev_value, it is the first transition if (!transition.prev_value) { - statusDuration[transition.value] = duration; + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; } // If there is no next_value, it is the last transition (the active one) else if (!transition.next_value) { if (statusDuration[transition.value]) { - statusDuration[transition.value] += duration; + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; } else { - statusDuration[transition.value] = duration; + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; } } // For all other transitions else { if (statusDuration[transition.value]) { - statusDuration[transition.value] += duration; + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; } else { - statusDuration[transition.value] = duration; + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; } } }); @@ -53,7 +64,6 @@ const jobLifecycle = async (req, res) => { } - const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const groupedTransitions = {}; @@ -66,6 +76,11 @@ const jobLifecycle = async (req, res) => { if (transition.end) { transition.end_readable = moment(transition.end).fromNow(); } + if(transition.duration){ + transition.duration_seconds = Math.round(transition.duration / 1000); + transition.duration_minutes = Math.round(transition.duration_seconds / 60); + transition.duration_readable = moment.duration(transition.duration).humanize(); + } return transition; }); @@ -75,7 +90,7 @@ const jobLifecycle = async (req, res) => { }; } - console.dir(groupedTransitions, {depth: null}); + console.dir(groupedTransitions, {depth: null}) return res.status(200).json({ jobIDs, From 5ea64ed805533994799e4e023a21dd84905344b1 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 24 Jan 2024 17:18:43 -0500 Subject: [PATCH 21/48] - Progress Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 178 +++++++++++------- client/src/utils/DateFormatter.jsx | 3 + server/graphql-client/queries.js | 2 +- server/job/job-lifecycle.js | 74 ++------ server/utils/calculateStatusDuration.js | 59 ++++++ server/utils/durationToHumanReadable.js | 22 +++ 6 files changed, 215 insertions(+), 123 deletions(-) create mode 100644 server/utils/calculateStatusDuration.js create mode 100644 server/utils/durationToHumanReadable.js diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 62ecb920a..3f48e826e 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,89 +1,135 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; +import moment from "moment"; import axios from 'axios'; -import {Card, Space, Table, Timeline} from 'antd'; -import {Bar, BarChart, CartesianGrid, LabelList, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import {Card, Space, Table} from 'antd'; +import {gql, useQuery} from "@apollo/client"; +import {DateTimeFormatterFunction} from "../../utils/DateFormatter"; +import {isEmpty} from "lodash"; +import {Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; + +const transformDataForChart = (durations) => { + const output = {}; + output.total = durations.total; + return durations.summations.forEach((summation) => { + output[summation.status] = summation.value + }); +}; +const getColor = (key) => { + // Generate a random color + const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16); + return randomColor; +}; + export function JobLifecycleComponent({job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); - useEffect(() => { - const getLifecycleData = async () => { - if (job && job.id) { - try { - setLoading(true); - const response = await axios.post("/job/lifecycle", {jobids: job.id}); - const data = response.data.transition[job.id]; - setLifecycleData(data); - } catch (err) { - console.error(`Error getting Job Lifecycle Data: ${err.message}`); - } finally { - setLoading(false); - } + // Used for tracking external state changes. + const {data} = useQuery(gql` + query get_job_test($id: uuid!){ + jobs_by_pk(id:$id){ + id + status } - }; + } + `, { + variables: { + id: job.id + }, + fetchPolicy: 'cache-only' + }); - getLifecycleData(); + /** + * Gets the lifecycle data for the job. + * @returns {Promise} + */ + const getLifecycleData = useCallback(async () => { + if (job && job.id) { + try { + setLoading(true); + const response = await axios.post("/job/lifecycle", {jobids: job.id}); + const data = response.data.transition[job.id]; + setLifecycleData(data); + } catch (err) { + console.error(`Error getting Job Lifecycle Data: ${err.message}`); + } finally { + setLoading(false); + } + } }, [job]); - const columnKeys = [ - 'start', 'end', 'value', 'prev_value', 'next_value', 'duration', 'type', 'created_at', 'updated_at', 'start_readable', 'end_readable','duration' + useEffect(() => { + if (!data) return; + setTimeout(() => { + getLifecycleData().catch(err => console.error(`Error getting Job Lifecycle Data: ${err.message}`)); + }, 1000); + }, [data, getLifecycleData]); + + const columns = [ + { + title: 'Value', + dataIndex: 'value', + key: 'value', + }, + { + title: 'Start', + dataIndex: 'start', + key: 'start', + render: (text) => DateTimeFormatterFunction(text), + sorter: (a, b) => moment(a.start).unix() - moment(b.start).unix(), + }, + { + title: 'Relative Start', + dataIndex: 'start_readable', + key: 'start_readable', + }, + { + title: 'End', + dataIndex: 'end', + key: 'end', + sorter: (a, b) => { + if (isEmpty(a.end) || isEmpty(b.end)) { + if (isEmpty(a.end) && isEmpty(b.end)) { + return 0; + } + return isEmpty(a.end) ? 1 : -1; + } + return moment(a.end).unix() - moment(b.end).unix(); + }, + render: (text) => isEmpty(text) ? 'N/A' : DateTimeFormatterFunction(text) + }, + { + title: 'Relative End', + dataIndex: 'end_readable', + key: 'end_readable', + }, + { + title: 'Duration', + dataIndex: 'duration_readable', + key: 'duration_readable', + sorter: (a, b) => a.duration - b.duration, + }, ]; - const columns = columnKeys.map(key => ({ - title: key.charAt(0).toUpperCase() + key.slice(1), - dataIndex: key, - key: key, - })); - - - const durationsData = useMemo(() => { - if (!lifecycleData) { - return []; - } - - const transformedData = Object.entries(lifecycleData.durations).map(([name, {value, humanReadable}]) => { - return { - name, - amt: value, - pv: humanReadable, - uv: value, - } - }) - - return [transformedData]; - }, [lifecycleData]); - - useEffect(() => { + console.log('LifeCycle Data'); console.dir(lifecycleData, {depth: null}) - console.dir(durationsData, {depth: null}) - - }, [lifecycleData,durationsData]); + }, [lifecycleData]); return ( {!loading ? ( - lifecycleData ? ( + lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? ( - - - {lifecycleData.lifecycle.map((item, index) => ( - - {item.value} - {item.start_readable} - - ))} - - - - + {lifecycleData.durations.summations.map((summation, idx) => { + + return ( + + ); + })} + + - +
diff --git a/client/src/utils/DateFormatter.jsx b/client/src/utils/DateFormatter.jsx index d034266e3..e8137c54d 100644 --- a/client/src/utils/DateFormatter.jsx +++ b/client/src/utils/DateFormatter.jsx @@ -17,6 +17,9 @@ export function DateTimeFormatter(props) { ) : null; } +export function DateTimeFormatterFunction(date) { + return moment(date).format("MM/DD/YYYY hh:mm a"); +} export function TimeFormatter(props) { return props.children ? moment(props.children).format(props.format ? props.format : "hh:mm a") diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 782bd3100..2bd83d1ab 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -519,7 +519,7 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = ` }`; exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) { - transitions(where: {jobid: {_in: $jobids}}, order_by: {created_at: asc}) { + transitions(where: {jobid: {_in: $jobids}}, order_by: {end: desc}) { start end value diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 07aaa037a..60d0c0be5 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,57 +1,14 @@ const _ = require("lodash"); const queries = require("../graphql-client/queries"); const moment = require("moment"); - -const calculateStatusDuration = (transitions) => { - let statusDuration = {}; - - transitions.forEach((transition, index) => { - let duration = transition.duration_minutes; - - // If there is no prev_value, it is the first transition - if (!transition.prev_value) { - statusDuration[transition.value] = { - value: duration, - humanReadable: transition.duration_readable - }; - } - // If there is no next_value, it is the last transition (the active one) - else if (!transition.next_value) { - if (statusDuration[transition.value]) { - statusDuration[transition.value].value += duration; - statusDuration[transition.value].humanReadable = transition.duration_readable; - } else { - statusDuration[transition.value] = { - value: duration, - humanReadable: transition.duration_readable - }; - } - } - // For all other transitions - else { - if (statusDuration[transition.value]) { - statusDuration[transition.value].value += duration; - statusDuration[transition.value].humanReadable = transition.duration_readable; - } else { - statusDuration[transition.value] = { - value: duration, - humanReadable: transition.duration_readable - }; - } - } - }); - - return statusDuration; -} - +const durationToHumanReadable = require("../utils/durationToHumanReadable"); +const calculateStatusDuration = require("../utils/calculateStatusDuration"); const jobLifecycle = async (req, res) => { const {jobids} = req.body; const jobIDs = _.isArray(jobids) ? jobids : [jobids]; - const client = req.userGraphQLClient; - const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobids: jobIDs,}); const transitions = resp.transitions; @@ -67,19 +24,21 @@ const jobLifecycle = async (req, res) => { const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const groupedTransitions = {}; - moment.relativeTimeThreshold('m', 30) + for (let jobId in transitionsByJobId) { let lifecycle = transitionsByJobId[jobId].map(transition => { - if (transition.start) { - transition.start_readable = moment(transition.start).fromNow(); - } - if (transition.end) { - transition.end_readable = moment(transition.end).fromNow(); - } - if(transition.duration){ - transition.duration_seconds = Math.round(transition.duration / 1000); - transition.duration_minutes = Math.round(transition.duration_seconds / 60); - transition.duration_readable = moment.duration(transition.duration).humanize(); + transition.start_readable = transition.start ? moment(transition.start).fromNow() : 'N/A'; + transition.end_readable = transition.end ? moment(transition.end).fromNow() : 'N/A'; + + if (transition.duration) { + transition.duration_seconds = Math.round(transition.duration / 1000); + transition.duration_minutes = Math.round(transition.duration_seconds / 60); + let duration = moment.duration(transition.duration); + transition.duration_readable = durationToHumanReadable(duration); + } else { + transition.duration_seconds = 0; + transition.duration_minutes = 0; + transition.duration_readable = 'N/A'; } return transition; }); @@ -90,13 +49,10 @@ const jobLifecycle = async (req, res) => { }; } - console.dir(groupedTransitions, {depth: null}) - return res.status(200).json({ jobIDs, transition: groupedTransitions, }); } - module.exports = jobLifecycle; \ No newline at end of file diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js new file mode 100644 index 000000000..8a74e09f8 --- /dev/null +++ b/server/utils/calculateStatusDuration.js @@ -0,0 +1,59 @@ +const moment = require('moment'); +const durationToHumanReadable = require("./durationToHumanReadable"); +/** + * Calculate the duration of each status of a job + * @param transitions + * @returns {{}} + */ +const calculateStatusDuration = (transitions) => { + let statusDuration = {}; + let totalDuration = 0; + let summations = []; + + transitions.forEach((transition, index) => { + let duration = transition.duration; + totalDuration += duration; + + if (!transition.prev_value) { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } else if (!transition.next_value) { + if (statusDuration[transition.value]) { + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; + } else { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } + } else { + if (statusDuration[transition.value]) { + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; + } else { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } + } + }); + + for (let [status, {value, humanReadable}] of Object.entries(statusDuration)) { + if (status !== 'total') { + summations.push({status, value, humanReadable}); + } + } + + const humanReadableTotal = durationToHumanReadable(moment.duration(totalDuration)); + + return { + summations, + total: totalDuration, + humanReadableTotal + }; +} +module.exports = calculateStatusDuration; \ No newline at end of file diff --git a/server/utils/durationToHumanReadable.js b/server/utils/durationToHumanReadable.js new file mode 100644 index 000000000..f13e24c98 --- /dev/null +++ b/server/utils/durationToHumanReadable.js @@ -0,0 +1,22 @@ +const durationToHumanReadable = (duration) => { + if (!duration) return 'N/A'; + + let parts = []; + + let years = duration.years(); + let months = duration.months(); + let days = duration.days(); + let hours = duration.hours(); + let minutes = duration.minutes(); + let seconds = duration.seconds(); + + if (years) parts.push(years + ' year' + (years > 1 ? 's' : '')); + if (months) parts.push(months + ' month' + (months > 1 ? 's' : '')); + if (days) parts.push(days + ' day' + (days > 1 ? 's' : '')); + if (hours) parts.push(hours + ' hour' + (hours > 1 ? 's' : '')); + if (minutes) parts.push(minutes + ' minute' + (minutes > 1 ? 's' : '')); + if (!minutes && !hours && !days && !months && !years && seconds) parts.push(seconds + ' second' + (seconds > 1 ? 's' : '')); + + return parts.join(', '); +} +module.exports = durationToHumanReadable; \ No newline at end of file From 6489a8666f8c4a6d94fc8be835a14392ea8ab853 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 24 Jan 2024 18:39:59 -0500 Subject: [PATCH 22/48] - Progress Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 67 +++++++++---------- server/utils/calculateStatusDuration.js | 2 +- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 3f48e826e..58475e300 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -5,24 +5,25 @@ import {Card, Space, Table} from 'antd'; import {gql, useQuery} from "@apollo/client"; import {DateTimeFormatterFunction} from "../../utils/DateFormatter"; import {isEmpty} from "lodash"; -import {Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; +import {Bar, BarChart, CartesianGrid, Legend, Tooltip, YAxis} from "recharts"; + const transformDataForChart = (durations) => { const output = {}; - output.total = durations.total; - return durations.summations.forEach((summation) => { - output[summation.status] = summation.value - }); -}; + // output.amt = durations.total; + // output.name = 'Total'; + durations.summations.forEach((summation) => { + output[summation.status] = summation.value; + }); + return [output]; +} const getColor = (key) => { // Generate a random color - const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16); + const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16); return randomColor; }; - export function JobLifecycleComponent({job, ...rest}) { - const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); @@ -125,33 +126,29 @@ export function JobLifecycleComponent({job, ...rest}) { - - - - - - - - {lifecycleData.durations.summations.map((summation, idx) => { - + + + + + + { + Object.keys(transformDataForChart(lifecycleData.durations)[0]).map((key) => { return ( - - ); - })} - - - - + + ) + }) + } + diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js index 8a74e09f8..ae0ef8238 100644 --- a/server/utils/calculateStatusDuration.js +++ b/server/utils/calculateStatusDuration.js @@ -11,7 +11,7 @@ const calculateStatusDuration = (transitions) => { let summations = []; transitions.forEach((transition, index) => { - let duration = transition.duration; + let duration = transition.duration_minutes; totalDuration += duration; if (!transition.prev_value) { From 03d4e4dcd133129dc77dfca0e31807ecff20dbdd Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 24 Jan 2024 16:01:48 -0800 Subject: [PATCH 23/48] IO-2543 AR Aging --- .../jobs-admin-remove-ar.component.jsx | 63 +++ .../report-center-modal.component.jsx | 38 +- client/src/graphql/jobs.queries.js | 450 +++++++++--------- .../src/pages/jobs-admin/jobs-admin.page.jsx | 5 +- client/src/translations/en_us/common.json | 3 + client/src/translations/es/common.json | 3 + client/src/translations/fr/common.json | 3 + client/src/utils/AuditTrailMappings.js | 66 +-- client/src/utils/TemplateConstants.js | 10 + 9 files changed, 366 insertions(+), 275 deletions(-) create mode 100644 client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx diff --git a/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx new file mode 100644 index 000000000..c85404f48 --- /dev/null +++ b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx @@ -0,0 +1,63 @@ +import { gql, useMutation } from "@apollo/client"; +import { Form, Switch, notification } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; + +const mapStateToProps = createStructuredSelector({}); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR); + +export function JobsAdminRemoveAR({ insertAuditTrail, job }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [switchValue, setSwitchValue] = useState(job.remove_from_ar); + + const [updateJob] = useMutation(gql` + mutation REMOVE_FROM_AR_JOB($jobId: uuid!, $remove_from_ar: Boolean!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { remove_from_ar: $remove_from_ar } + ) { + id + remove_from_ar + } + } + `); + + const handleChange = async (value) => { + setLoading(true); + const result = await updateJob({ + variables: { jobId: job.id, remove_from_ar: value }, + }); + + if (!result.errors) { + notification["success"]({ message: t("jobs.successes.save") }); + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.admin_job_remove_from_ar(value), + }); + setSwitchValue(value); + } else { + notification["error"]({ + message: t("jobs.errors.saving", { + error: JSON.stringify(result.errors), + }), + }); + } + setLoading(false); + }; + + return ( + + + + ); +} diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index 913d27282..e37e02164 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -239,20 +239,30 @@ export function ReportCenterModalComponent({ reportCenterModal }) { else return null; }} - - + + {() => { + const key = form.getFieldValue("key"); + const datedisable = Templates[key] && Templates[key].datedisable; + if (datedisable !== true) { + return ( + + + + ); + } else return null; + }} {() => { diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 4a1c85036..adfc42765 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -545,147 +545,166 @@ export const QUERY_JOB_COSTING_DETAILS = gql` export const GET_JOB_BY_PK = gql` query GET_JOB_BY_PK($id: uuid!) { jobs_by_pk(id: $id) { - updated_at + actual_completion + actual_delivery + actual_in + adjustment_bottom_line + area_of_damage + auto_add_ats + available_jobs { + id + } + alt_transport + ca_bc_pvrt + ca_customer_gst + ca_gst_registrant + category + cccontracts { + agreementnumber + courtesycar { + fleetnumber + id + make + model + plate + year + } + id + scheduledreturn + start + status + } + cieca_ttl + class + clm_no + clm_total + comment + converted + csiinvites { + completedon + id + } + date_estimated + date_exported + date_invoiced + date_last_contacted + date_lost_sale + date_next_contact + date_open + date_rentalresp + date_repairstarted + date_scheduled + date_towin + date_void + ded_amt + ded_note + ded_status + deliverchecklist + depreciation_taxes + driveable + employee_body employee_body_rel { id first_name last_name } - employee_refinish_rel { - id - first_name - last_name - } - employee_prep_rel { - id - first_name - last_name - } + employee_csr employee_csr_rel { id first_name last_name } - employee_csr employee_prep + employee_prep_rel { + id + first_name + last_name + } employee_refinish - employee_body - alt_transport - intakechecklist - invoice_final_note - comment - loss_desc - kmin - kmout - referral_source - referral_source_extra - unit_number - po_number - special_coverage_policy - scheduled_delivery - converted - lbr_adjustments - ro_number - po_number - clm_total + employee_refinish_rel { + id + first_name + last_name + } + est_co_nm + est_ct_fn + est_ct_ln + est_ea + est_ph1 + federal_tax_rate + id inproduction - vehicleid - plate_no - plate_st - v_vin - v_model_yr - v_model_desc - v_make_desc - v_color - vehicleid - driveable - towin - loss_of_use - lost_sale_reason - vehicle { - id - plate_no - plate_st - v_vin - v_model_yr - v_model_desc - v_make_desc - v_color - notes - v_paint_codes - jobs { - id - ro_number - status - clm_no - } - } - available_jobs { - id - } - ins_co_id - policy_no - loss_date - clm_no - area_of_damage - ins_co_nm ins_addr1 ins_city + ins_co_id + ins_co_nm ins_ct_ln ins_ct_fn ins_ea ins_ph1 - est_co_nm - est_ct_fn - est_ct_ln - est_ph1 - est_ea - selling_dealer - servicing_dealer - selling_dealer_contact - servicing_dealer_contact - regie_number - scheduled_completion - id - ded_amt - ded_status - depreciation_taxes - other_amount_payable - towing_payable - storage_payable - adjustment_bottom_line - federal_tax_rate - state_tax_rate - local_tax_rate - tax_tow_rt - tax_str_rt - tax_paint_mat_rt - tax_shop_mat_rt - tax_sub_rt - tax_lbr_rt - tax_levies_rt - parts_tax_rates - job_totals - ownr_fn - ownr_ln - ownr_co_nm - ownr_ea - ownr_addr1 - ownr_addr2 - ownr_city - ownr_st - ownr_zip - ownr_ctry - ownr_ph1 - ownr_ph2 - production_vars - ca_gst_registrant - ownerid - ded_note - materials - auto_add_ats - rate_ats + intakechecklist + invoice_final_note iouparent + job_totals + joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { + act_price + ah_detail_line + alt_partm + alt_partno + billlines(limit: 1, order_by: { bill: { date: desc } }) { + actual_cost + actual_price + bill { + id + invoice_number + vendor { + id + name + } + } + joblineid + id + quantity + } + convertedtolbr + critical + db_hrs + db_price + db_ref + id + ioucreated + lbr_amt + lbr_op + line_desc + line_ind + line_no + line_ref + location + manual_line + mod_lb_hrs + mod_lbr_ty + notes + oem_partno + op_code_desc + part_qty + part_type + prt_dsmk_m + prt_dsmk_p + status + tax_part + unq_seq + } + kmin + kmout + labor_rate_desc + lbr_adjustments + local_tax_rate + loss_date + loss_desc + loss_of_use + lost_sale_reason + materials + other_amount_payable owner { id ownr_fn @@ -702,7 +721,40 @@ export const GET_JOB_BY_PK = gql` ownr_ph2 tax_number } - labor_rate_desc + owner_owing + ownerid + ownr_addr1 + ownr_addr2 + ownr_ctry + ownr_city + ownr_co_nm + ownr_ea + ownr_fn + ownr_ln + ownr_ph1 + ownr_ph2 + ownr_st + ownr_zip + parts_tax_rates + payments { + amount + created_at + date + exportedat + id + jobid + memo + payer + paymentnum + transactionid + type + } + plate_no + plate_st + po_number + policy_no + production_vars + rate_ats rate_la1 rate_la2 rate_la3 @@ -726,121 +778,64 @@ export const GET_JOB_BY_PK = gql` rate_mapa rate_mash rate_matd - actual_in - federal_tax_rate - local_tax_rate - state_tax_rate + regie_number + referral_source + referral_source_extra + remove_from_ar + ro_number scheduled_completion - scheduled_in - actual_completion scheduled_delivery - actual_delivery - date_estimated - date_open - date_scheduled - date_invoiced - date_last_contacted - date_lost_sale - date_next_contact - date_towin - date_rentalresp - date_exported - date_repairstarted - date_void + scheduled_in + selling_dealer + servicing_dealer + selling_dealer_contact + servicing_dealer_contact + special_coverage_policy + state_tax_rate status - owner_owing - tax_registration_number - class - category - deliverchecklist - voided - ca_bc_pvrt - ca_customer_gst + storage_payable suspended - joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { + tax_lbr_rt + tax_levies_rt + tax_paint_mat_rt + tax_registration_number + tax_shop_mat_rt + tax_str_rt + tax_sub_rt + tax_tow_rt + towin + towing_payable + unit_number + updated_at + v_vin + v_model_yr + v_model_desc + v_make_desc + v_color + vehicleid + vehicle { id - alt_partm - line_no - unq_seq - line_ind - line_desc - line_ref - part_type - oem_partno - alt_partno - db_price - act_price - part_qty - mod_lbr_ty - db_hrs - mod_lb_hrs - lbr_op - lbr_amt - op_code_desc - status + jobs { + clm_no + id + ro_number + status + } notes - location - tax_part - db_ref - manual_line - prt_dsmk_p - prt_dsmk_m - ioucreated - convertedtolbr - ah_detail_line - critical - billlines(limit: 1, order_by: { bill: { date: desc } }) { - id - quantity - actual_cost - actual_price - joblineid - bill { - id - invoice_number - vendor { - id - name - } - } - } - } - payments { - id - jobid - amount - payer - paymentnum - created_at - transactionid - memo - date - type - exportedat - } - cccontracts { - id - status - start - scheduledreturn - agreementnumber - courtesycar { - id - make - model - year - plate - fleetnumber - } - } - cieca_ttl - csiinvites { - id - completedon + plate_no + plate_st + v_color + v_make_desc + v_model_desc + v_model_yr + v_paint_codes + v_vin } + voided } } `; + export const GET_JOB_RECONCILIATION_BY_PK = gql` query GET_JOB_RECONCILIATION_BY_PK($id: uuid!) { bills(where: { jobid: { _eq: $id } }) { @@ -905,6 +900,7 @@ export const GET_JOB_RECONCILIATION_BY_PK = gql` } } `; + export const QUERY_JOB_CARD_DETAILS = gql` query QUERY_JOB_CARD_DETAILS($id: uuid!) { jobs_by_pk(id: $id) { diff --git a/client/src/pages/jobs-admin/jobs-admin.page.jsx b/client/src/pages/jobs-admin/jobs-admin.page.jsx index 0d08e5114..0c7eb9a33 100644 --- a/client/src/pages/jobs-admin/jobs-admin.page.jsx +++ b/client/src/pages/jobs-admin/jobs-admin.page.jsx @@ -7,16 +7,16 @@ import { useParams } from "react-router-dom"; import AlertComponent from "../../components/alert/alert.component"; import JobCalculateTotals from "../../components/job-calculate-totals/job-calculate-totals.component"; import ScoreboardAddButton from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component"; +import JobsAdminStatus from "../../components/jobs-admin-change-status/jobs-admin-change.status.component"; import JobsAdminClass from "../../components/jobs-admin-class/jobs-admin-class.component"; import JobsAdminDatesChange from "../../components/jobs-admin-dates/jobs-admin-dates.component"; import JobsAdminDeleteIntake from "../../components/jobs-admin-delete-intake/jobs-admin-delete-intake.component"; import JobsAdminMarkReexport from "../../components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component"; import JobAdminOwnerReassociate from "../../components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component"; +import JobsAdminRemoveAR from "../../components/jobs-admin-remove-ar/jobs-admin-remove-ar.component"; import JobsAdminUnvoid from "../../components/jobs-admin-unvoid/jobs-admin-unvoid.component"; import JobAdminVehicleReassociate from "../../components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; -import JobsAdminStatus from "../../components/jobs-admin-change-status/jobs-admin-change.status.component"; - import NotFound from "../../components/not-found/not-found.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { GET_JOB_BY_PK } from "../../graphql/jobs.queries"; @@ -104,6 +104,7 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) { + diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 919f7e297..1589e9b5f 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -99,6 +99,7 @@ }, "audit_trail": { "messages": { + "admin_job_remove_from_ar": "ADMIN: Remove from AR updated to: {{status}}", "admin_jobmarkexported": "ADMIN: Job marked as exported.", "admin_jobmarkforreexport": "ADMIN: Job marked for re-export.", "admin_jobuninvoice": "ADMIN: Job has been uninvoiced.", @@ -1833,6 +1834,7 @@ }, "reconciliationheader": "Parts & Sublet Reconciliation", "relatedros": "Related ROs", + "remove_from_ar": "Remove from AR", "returntotals": "Return Totals", "rosaletotal": "RO Parts Total", "sale_additional": "Sales - Additional", @@ -2563,6 +2565,7 @@ }, "templates": { "anticipated_revenue": "Anticipated Revenue", + "ar_aging": "AR Aging", "attendance_detail": "Attendance (All Employees)", "attendance_employee": "Employee Attendance", "attendance_summary": "Attendance Summary (All Employees)", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index a798f790c..fc977e7eb 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -99,6 +99,7 @@ }, "audit_trail": { "messages": { + "admin_job_remove_from_ar": "", "admin_jobmarkexported": "", "admin_jobmarkforreexport": "", "admin_jobuninvoice": "", @@ -1833,6 +1834,7 @@ }, "reconciliationheader": "", "relatedros": "", + "remove_from_ar": "", "returntotals": "", "rosaletotal": "", "sale_additional": "", @@ -2563,6 +2565,7 @@ }, "templates": { "anticipated_revenue": "", + "ar_aging": "", "attendance_detail": "", "attendance_employee": "", "attendance_summary": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index b65abaf04..76e3ce6a9 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -99,6 +99,7 @@ }, "audit_trail": { "messages": { + "admin_job_remove_from_ar": "", "admin_jobmarkexported": "", "admin_jobmarkforreexport": "", "admin_jobuninvoice": "", @@ -1833,6 +1834,7 @@ }, "reconciliationheader": "", "relatedros": "", + "remove_from_ar": "", "returntotals": "", "rosaletotal": "", "sale_additional": "", @@ -2563,6 +2565,7 @@ }, "templates": { "anticipated_revenue": "", + "ar_aging": "", "attendance_detail": "", "attendance_employee": "", "attendance_summary": "", diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index d7098fa2d..eefbb3a11 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -1,54 +1,56 @@ import i18n from "i18next"; const AuditTrailMapping = { - alertToggle: (status) => i18n.t("audit_trail.messages.alerttoggle", { status }), + admin_job_remove_from_ar: (status) => + i18n.t("audit_trail.messages.admin_job_remove_from_ar", { status }), + admin_jobfieldchange: (field, value) => + "ADMIN: " + + i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), + admin_jobmarkexported: () => + i18n.t("audit_trail.messages.admin_jobmarkexported"), + admin_jobmarkforreexport: () => + i18n.t("audit_trail.messages.admin_jobmarkforreexport"), + admin_jobstatuschange: (status) => + "ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }), + admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"), + admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"), + alertToggle: (status) => + i18n.t("audit_trail.messages.alerttoggle", { status }), appointmentcancel: (lost_sale_reason) => i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }), appointmentinsert: (start) => i18n.t("audit_trail.messages.appointmentinsert", { start }), - jobstatuschange: (status) => - i18n.t("audit_trail.messages.jobstatuschange", { status }), - admin_jobstatuschange: (status) => - "ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }), - jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), - jobimported: () => i18n.t("audit_trail.messages.jobimported"), - jobinvoiced: () => - i18n.t("audit_trail.messages.jobinvoiced"), - jobconverted: (ro_number) => - i18n.t("audit_trail.messages.jobconverted", { ro_number }), - jobfieldchange: (field, value) => - i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), - admin_jobfieldchange: (field, value) => - "ADMIN: " + - i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), - jobspartsorder: (order_number) => - i18n.t("audit_trail.messages.jobspartsorder", { order_number }), - jobspartsreturn: (order_number) => - i18n.t("audit_trail.messages.jobspartsreturn", { order_number }), - jobmodifylbradj: ({ mod_lbr_ty, hours }) => - i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }), billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }), billupdated: (invoice_number) => i18n.t("audit_trail.messages.billupdated", { invoice_number }), + failedpayment: () => i18n.t("audit_trail.messages.failedpayment"), jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }), jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }), - jobinproductionchange: (inproduction) => - i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }), jobchecklist: (type, inproduction, status) => i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }), + jobconverted: (ro_number) => + i18n.t("audit_trail.messages.jobconverted", { ro_number }), + jobfieldchange: (field, value) => + i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), + jobimported: () => i18n.t("audit_trail.messages.jobimported"), + jobinproductionchange: (inproduction) => + i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }), + jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"), + jobmodifylbradj: ({ mod_lbr_ty, hours }) => + i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }), jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"), - jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"), jobnotedeleted: () => i18n.t("audit_trail.messages.jobnotedeleted"), - admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"), - admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"), - admin_jobmarkforreexport: () => - i18n.t("audit_trail.messages.admin_jobmarkforreexport"), - admin_jobmarkexported: () => - i18n.t("audit_trail.messages.admin_jobmarkexported"), - failedpayment: () => i18n.t("audit_trail.messages.failedpayment"), + jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"), + jobspartsorder: (order_number) => + i18n.t("audit_trail.messages.jobspartsorder", { order_number }), + jobspartsreturn: (order_number) => + i18n.t("audit_trail.messages.jobspartsreturn", { order_number }), + jobstatuschange: (status) => + i18n.t("audit_trail.messages.jobstatuschange", { status }), + jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), }; export default AuditTrailMapping; diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index eeb937c3a..ad614af3b 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2020,6 +2020,7 @@ export const TemplateList = (type, context) => { key: "lost_sales", //idtype: "vendor", disabled: false, + datedisable: true, rangeFilter: { object: i18n.t("reportcenter.labels.objects.jobs"), field: i18n.t("jobs.fields.date_lost_sale"), @@ -2039,6 +2040,15 @@ export const TemplateList = (type, context) => { }, group: "jobs", }, + ar_aging: { + title: i18n.t("reportcenter.templates.ar_aging"), + subject: i18n.t("reportcenter.templates.ar_aging"), + key: "ar_aging", + //idtype: "vendor", + disabled: false, + datedisable: true, + group: "customers", + }, } : {}), ...(!type || type === "courtesycarcontract" From 36dd97394f99db593026281ffff88a2d78c3a94c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 24 Jan 2024 16:15:32 -0800 Subject: [PATCH 24/48] IO-2543 Adjust for having no dates --- .../report-center-modal/report-center-modal.component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index e37e02164..c4bef11c3 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -68,8 +68,8 @@ export function ReportCenterModalComponent({ reportCenterModal }) { const handleFinish = async (values) => { setLoading(true); - const start = values.dates[0]; - const end = values.dates[1]; + const start = values.dates ? values.dates[0] : null; + const end = values.dates ? values.dates[1] : null; const { id } = values; await GenerateDocument( From eb8519dc1d0ddb982c58a6df0d180f8fedf1cb07 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 24 Jan 2024 18:37:41 -0800 Subject: [PATCH 25/48] IO-2543 Move queries to jobs.queries.js and correct layout --- .../jobs-admin-change.status.component.jsx | 14 ++- .../jobs-admin-delete-intake.component.jsx | 56 ++++----- .../jobs-admin-mark-reexport.component.jsx | 118 +++++++----------- .../jobs-admin-remove-ar.component.jsx | 36 +++--- .../jobs-admin-unvoid.component.jsx | 76 +++-------- client/src/graphql/jobs.queries.js | 117 +++++++++++++++++ 6 files changed, 230 insertions(+), 187 deletions(-) diff --git a/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx b/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx index edef81343..54a020d37 100644 --- a/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx +++ b/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx @@ -53,12 +53,14 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) { ); return ( - - - + + + + ); } diff --git a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx index 190778437..ec1bd976b 100644 --- a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx +++ b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx @@ -1,34 +1,18 @@ import { useMutation } from "@apollo/client"; -import { Button, notification } from "antd"; -import { gql } from "@apollo/client"; +import { Button, Space, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; +import { + DELETE_DELIVERY_CHECKLIST, + DELETE_INTAKE_CHECKLIST, +} from "../../graphql/jobs.queries"; + export default function JobAdminDeleteIntake({ job }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [deleteIntake] = useMutation(gql` - mutation DELETE_INTAKE($jobId: uuid!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { intakechecklist: null } - ) { - id - intakechecklist - } - } - `); - const [DELETE_DELIVERY] = useMutation(gql` - mutation DELETE_DELIVERY($jobId: uuid!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { deliverchecklist: null } - ) { - id - deliverchecklist - } - } - `); + const [deleteIntake] = useMutation(DELETE_INTAKE_CHECKLIST); + const [deleteDelivery] = useMutation(DELETE_DELIVERY_CHECKLIST); const handleDelete = async (values) => { setLoading(true); @@ -50,7 +34,7 @@ export default function JobAdminDeleteIntake({ job }) { const handleDeleteDelivery = async (values) => { setLoading(true); - const result = await DELETE_DELIVERY({ + const result = await deleteDelivery({ variables: { jobId: job.id }, }); @@ -68,12 +52,22 @@ export default function JobAdminDeleteIntake({ job }) { return ( <> - - + + + + ); } diff --git a/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx b/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx index c47c30def..ae193fa72 100644 --- a/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx +++ b/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx @@ -1,5 +1,5 @@ -import { gql, useMutation } from "@apollo/client"; -import { Button, notification } from "antd"; +import { useMutation } from "@apollo/client"; +import { Button, Space, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -7,6 +7,11 @@ import moment from "moment"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; +import { + MARK_JOB_AS_EXPORTED, + MARK_JOB_AS_UNINVOICED, + MARK_JOB_FOR_REEXPORT, +} from "../../graphql/jobs.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, @@ -35,58 +40,18 @@ export function JobAdminMarkReexport({ const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); - const [markJobForReexport] = useMutation(gql` - mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { date_exported: null - status: "${bodyshop.md_ro_statuses.default_invoiced}" - } - ) { - id - date_exported - status - date_invoiced - } - } - `); - const [markJobExported] = useMutation(gql` - mutation MARK_JOB_AS_EXPORTED($jobId: uuid!, $date_exported: timestamptz!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { date_exported: $date_exported - status: "${bodyshop.md_ro_statuses.default_exported}" - } - ) { - id - date_exported - date_invoiced - status - } - } - `); - const [markJobUninvoiced] = useMutation(gql` - mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, ) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { date_exported: null - date_invoiced: null - status: "${bodyshop.md_ro_statuses.default_delivered}" - } - ) { - id - date_exported - date_invoiced - status - } - } - `); + const [markJobForReexport] = useMutation(MARK_JOB_FOR_REEXPORT); + const [markJobExported] = useMutation(MARK_JOB_AS_EXPORTED); + const [markJobUninvoiced] = useMutation(MARK_JOB_AS_UNINVOICED); const handleMarkForExport = async () => { setLoading(true); const result = await markJobForReexport({ - variables: { jobId: job.id }, + variables: { + jobId: job.id, + default_invoiced: bodyshop.md_ro_statuses.default_invoiced, + }, }); if (!result.errors) { @@ -108,7 +73,11 @@ export function JobAdminMarkReexport({ const handleMarkExported = async () => { setLoading(true); const result = await markJobExported({ - variables: { jobId: job.id, date_exported: moment() }, + variables: { + jobId: job.id, + date_exported: moment(), + default_exported: bodyshop.md_ro_statuses.default_exported, + }, }); await insertExportLog({ @@ -144,7 +113,10 @@ export function JobAdminMarkReexport({ const handleUninvoice = async () => { setLoading(true); const result = await markJobUninvoiced({ - variables: { jobId: job.id }, + variables: { + jobId: job.id, + default_delivered: bodyshop.md_ro_statuses.default_delivered, + }, }); if (!result.errors) { @@ -165,27 +137,29 @@ export function JobAdminMarkReexport({ return ( <> - - - + + + + + ); } diff --git a/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx index c85404f48..f1bda15ce 100644 --- a/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx +++ b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx @@ -1,9 +1,10 @@ -import { gql, useMutation } from "@apollo/client"; -import { Form, Switch, notification } from "antd"; +import { useMutation } from "@apollo/client"; +import { Switch, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { UPDATE_REMOVE_FROM_AR } from "../../graphql/jobs.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; @@ -20,21 +21,11 @@ export function JobsAdminRemoveAR({ insertAuditTrail, job }) { const [loading, setLoading] = useState(false); const [switchValue, setSwitchValue] = useState(job.remove_from_ar); - const [updateJob] = useMutation(gql` - mutation REMOVE_FROM_AR_JOB($jobId: uuid!, $remove_from_ar: Boolean!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { remove_from_ar: $remove_from_ar } - ) { - id - remove_from_ar - } - } - `); + const [mutationUpdateRemoveFromAR] = useMutation(UPDATE_REMOVE_FROM_AR); const handleChange = async (value) => { setLoading(true); - const result = await updateJob({ + const result = await mutationUpdateRemoveFromAR({ variables: { jobId: job.id, remove_from_ar: value }, }); @@ -56,8 +47,19 @@ export function JobsAdminRemoveAR({ insertAuditTrail, job }) { }; return ( - - - + <> +
+
+ {t("jobs.labels.remove_from_ar")}: +
+
+ +
+
+ ); } diff --git a/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx b/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx index 7963fd05f..2094178c4 100644 --- a/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx +++ b/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx @@ -1,9 +1,10 @@ -import { gql, useMutation } from "@apollo/client"; +import { useMutation } from "@apollo/client"; import { Button, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { UNVOID_JOB } from "../../graphql/jobs.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, @@ -29,66 +30,17 @@ export function JobsAdminUnvoid({ }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [updateJob] = useMutation(gql` -mutation UNVOID_JOB($jobId: uuid!) { - update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${ - bodyshop.md_ro_statuses.default_imported - }", date_void: null}) { - id - date_void - voided - status - } - insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${ - currentUser.email - }", text: "${t("jobs.labels.unvoidnote")}"}) { - returning { - id - } - } -} - - `); - - // const result = await voidJob({ - // variables: { - // jobId: job.id, - // job: { - // status: bodyshop.md_ro_statuses.default_void, - // voided: true, - // }, - // note: [ - // { - // jobid: job.id, - // created_by: currentUser.email, - // audit: true, - // text: t("jobs.labels.voidnote", { - // date: moment().format("MM/DD/yyy"), - // time: moment().format("hh:mm a"), - // }), - // }, - // ], - // }, - // }); - - // if (!!!result.errors) { - // notification["success"]({ - // message: t("jobs.successes.voided"), - // }); - // //go back to jobs list. - // history.push(`/manage/`); - // } else { - // notification["error"]({ - // message: t("jobs.errors.voiding", { - // error: JSON.stringify(result.errors), - // }), - // }); - // } + const [mutationUnvoidJob] = useMutation(UNVOID_JOB); const handleUpdate = async (values) => { setLoading(true); - const result = await updateJob({ - variables: { jobId: job.id }, + const result = await mutationUnvoidJob({ + variables: { + jobId: job.id, + default_imported: bodyshop.md_ro_statuses.default_imported, + currentUserEmail: currentUser.email, + text: t("jobs.labels.unvoidnote"), + }, }); if (!result.errors) { @@ -110,8 +62,10 @@ mutation UNVOID_JOB($jobId: uuid!) { }; return ( - + <> + + ); } diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index adfc42765..57bb935f8 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -2221,3 +2221,120 @@ export const GET_JOB_LINE_ORDERS = gql` } } `; + +export const UPDATE_REMOVE_FROM_AR = gql` + mutation UPDATE_REMOVE_FROM_AR($jobId: uuid!, $remove_from_ar: Boolean!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { remove_from_ar: $remove_from_ar } + ) { + id + remove_from_ar + } + } +`; + +export const UNVOID_JOB = gql` + mutation UNVOID_JOB( + $jobId: uuid! + $default_imported: String! + $currentUserEmail: String! + $text: String! + ) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { voided: false, status: $default_imported, date_void: null } + ) { + id + date_void + voided + status + } + insert_notes( + objects: { + jobid: $jobId + audit: true + created_by: $currentUserEmail + text: $text + } + ) { + returning { + id + } + } + } +`; + +export const DELETE_INTAKE_CHECKLIST = gql` + mutation DELETE_INTAKE($jobId: uuid!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { intakechecklist: null } + ) { + id + intakechecklist + } + } +`; + +export const DELETE_DELIVERY_CHECKLIST = gql` + mutation DELETE_DELIVERY($jobId: uuid!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { deliverchecklist: null } + ) { + id + deliverchecklist + } + } +`; + +export const MARK_JOB_FOR_REEXPORT = gql` + mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!, $default_invoiced: String!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { date_exported: null, status: $default_invoiced } + ) { + id + date_exported + status + date_invoiced + } + } +`; + +export const MARK_JOB_AS_EXPORTED = gql` + mutation MARK_JOB_AS_EXPORTED( + $jobId: uuid! + $date_exported: timestamptz! + $default_exported: String! + ) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { date_exported: $date_exported, status: $default_exported } + ) { + id + date_exported + date_invoiced + status + } + } +`; + +export const MARK_JOB_AS_UNINVOICED = gql` + mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, $default_delivered: String!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { + date_exported: null + date_invoiced: null + status: $default_delivered + } + ) { + id + date_exported + date_invoiced + status + } + } +`; From f8408908b22ea705a954cc7198b11c8db96be0b3 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 24 Jan 2024 21:40:05 -0500 Subject: [PATCH 26/48] - Major Progress Commit Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 131 ++++++++++++------ .../job-lifecycle/job-lifecycle.styles.scss | 0 .../jobs-detail.page.component.jsx | 17 +-- server/utils/calculateStatusDuration.js | 39 ++++-- 4 files changed, 126 insertions(+), 61 deletions(-) create mode 100644 client/src/components/job-lifecycle/job-lifecycle.styles.scss diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 58475e300..6ddbcfc51 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,27 +1,12 @@ import React, {useCallback, useEffect, useState} from 'react'; import moment from "moment"; import axios from 'axios'; -import {Card, Space, Table} from 'antd'; +import {Badge, Card, Space, Table, Tag} from 'antd'; import {gql, useQuery} from "@apollo/client"; import {DateTimeFormatterFunction} from "../../utils/DateFormatter"; import {isEmpty} from "lodash"; -import {Bar, BarChart, CartesianGrid, Legend, Tooltip, YAxis} from "recharts"; - -const transformDataForChart = (durations) => { - const output = {}; - // output.amt = durations.total; - // output.name = 'Total'; - durations.summations.forEach((summation) => { - output[summation.status] = summation.value; - }); - return [output]; -} -const getColor = (key) => { - // Generate a random color - const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16); - return randomColor; -}; +require('./job-lifecycle.styles.scss'); export function JobLifecycleComponent({job, ...rest}) { const [loading, setLoading] = useState(true); @@ -124,34 +109,92 @@ export function JobLifecycleComponent({job, ...rest}) { {!loading ? ( lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? ( - - - - - - - - { - Object.keys(transformDataForChart(lifecycleData.durations)[0]).map((key) => { - return ( - - ) - }) - } - + + + Statuses + + + )} + style={{width: '100%'}} + > +
+ {lifecycleData.durations.summations.map((key, index, array) => { + const isFirst = index === 0; + const isLast = index === array.length - 1; + + return ( +
+ {Math.round(key.percentage)}% +
+ ); + })} +
+ +
+ {lifecycleData.durations.summations.map((key) => ( + +
+ {key.status} ({key.roundedPercentage}) +
+
+ ))} +
-
- + + Accumulated Time: {lifecycleData.durations.humanReadableTotal} + + + + + + Transitions + + + )}> +
diff --git a/client/src/components/job-lifecycle/job-lifecycle.styles.scss b/client/src/components/job-lifecycle/job-lifecycle.styles.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index 0f335d982..b2e8159c8 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -289,13 +289,6 @@ export function JobsDetailPage({ form={form} /> - Lifecycle} - key="lifecycle" - > - - - Lifecycle} + key="lifecycle" + > + + + + diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js index ae0ef8238..b7d432e32 100644 --- a/server/utils/calculateStatusDuration.js +++ b/server/utils/calculateStatusDuration.js @@ -1,17 +1,23 @@ -const moment = require('moment'); const durationToHumanReadable = require("./durationToHumanReadable"); -/** - * Calculate the duration of each status of a job - * @param transitions - * @returns {{}} - */ +const moment = require("moment"); +const _ = require("lodash"); +const crypto = require('crypto'); + +const getColor = (key) => { + const hash = crypto.createHash('sha256'); + hash.update(key); + const hashedKey = hash.digest('hex'); + const num = parseInt(hashedKey, 16); + return '#' + (num % 16777215).toString(16).padStart(6, '0'); +}; + const calculateStatusDuration = (transitions) => { let statusDuration = {}; let totalDuration = 0; let summations = []; transitions.forEach((transition, index) => { - let duration = transition.duration_minutes; + let duration = transition.duration; totalDuration += duration; if (!transition.prev_value) { @@ -42,16 +48,31 @@ const calculateStatusDuration = (transitions) => { } }); + // Calculate the percentage for each status +// Calculate the percentage for each status + let totalPercentage = 0; + const statusKeys = Object.keys(statusDuration); + statusKeys.forEach((status, index) => { + if (index !== statusKeys.length - 1) { + const percentage = (statusDuration[status].value / totalDuration) * 100; + totalPercentage += percentage; + statusDuration[status].percentage = percentage; + } else { + statusDuration[status].percentage = 100 - totalPercentage; + } + }); + for (let [status, {value, humanReadable}] of Object.entries(statusDuration)) { if (status !== 'total') { - summations.push({status, value, humanReadable}); + summations.push({status, value, humanReadable, percentage: statusDuration[status].percentage, color: getColor(status), roundedPercentage: `${Math.round(statusDuration[status].percentage)}%`}); } } const humanReadableTotal = durationToHumanReadable(moment.duration(totalDuration)); return { - summations, + summations: _.orderBy(summations, ['value'], ['asc']), + totalStatuses: summations.length, total: totalDuration, humanReadableTotal }; From a394d6b37e491b72e667d1077e4be52679274b60 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 25 Jan 2024 12:38:32 -0500 Subject: [PATCH 27/48] - Major Progress Commit Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 6ddbcfc51..07cb778c9 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -99,10 +99,10 @@ export function JobLifecycleComponent({job, ...rest}) { }, ]; - useEffect(() => { - console.log('LifeCycle Data'); - console.dir(lifecycleData, {depth: null}) - }, [lifecycleData]); + // useEffect(() => { + // console.log('LifeCycle Data'); + // console.dir(lifecycleData, {depth: null}) + // }, [lifecycleData]); return ( @@ -144,14 +144,12 @@ export function JobLifecycleComponent({job, ...rest}) { alignItems: 'center', margin: 0, padding: 0, + borderTop: '1px solid #f0f2f5', borderBottom: '1px solid #f0f2f5', borderLeft: isFirst ? '1px solid #f0f2f5' : undefined, borderRight: isLast ? '1px solid #f0f2f5' : undefined, - borderBottomLeftRadius: isFirst ? '5px' : undefined, - borderTopLeftRadius: isFirst ? '5px' : undefined, - borderBottomRightRadius: isLast ? '5px' : undefined, - borderTopRightRadius: isLast ? '5px' : undefined, + backgroundColor: key.color, width: `${key.percentage}%` }} From 50f84d40e1dc13d0143fd68c293b5a695bda8dfb Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 25 Jan 2024 10:30:03 -0800 Subject: [PATCH 28/48] Allow negative balance for AR. --- .../1706207204357_run_sql_migration/down.sql | 35 +++++++++++++++++++ .../1706207204357_run_sql_migration/up.sql | 33 +++++++++++++++++ .../1706207267558_run_sql_migration/down.sql | 35 +++++++++++++++++++ .../1706207267558_run_sql_migration/up.sql | 33 +++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 hasura/migrations/1706207204357_run_sql_migration/down.sql create mode 100644 hasura/migrations/1706207204357_run_sql_migration/up.sql create mode 100644 hasura/migrations/1706207267558_run_sql_migration/down.sql create mode 100644 hasura/migrations/1706207267558_run_sql_migration/up.sql diff --git a/hasura/migrations/1706207204357_run_sql_migration/down.sql b/hasura/migrations/1706207204357_run_sql_migration/down.sql new file mode 100644 index 000000000..1deabac64 --- /dev/null +++ b/hasura/migrations/1706207204357_run_sql_migration/down.sql @@ -0,0 +1,35 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced, +-- j.shopid +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1706207204357_run_sql_migration/up.sql b/hasura/migrations/1706207204357_run_sql_migration/up.sql new file mode 100644 index 000000000..6a42b8d7a --- /dev/null +++ b/hasura/migrations/1706207204357_run_sql_migration/up.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced, + j.shopid +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; + + +END +$function$; diff --git a/hasura/migrations/1706207267558_run_sql_migration/down.sql b/hasura/migrations/1706207267558_run_sql_migration/down.sql new file mode 100644 index 000000000..1deabac64 --- /dev/null +++ b/hasura/migrations/1706207267558_run_sql_migration/down.sql @@ -0,0 +1,35 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced, +-- j.shopid +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1706207267558_run_sql_migration/up.sql b/hasura/migrations/1706207267558_run_sql_migration/up.sql new file mode 100644 index 000000000..6a42b8d7a --- /dev/null +++ b/hasura/migrations/1706207267558_run_sql_migration/up.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced, + j.shopid +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; + + +END +$function$; From 0e4f5b8b2ac1c2be6a59132ef67775aa41a8c8d9 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 25 Jan 2024 13:35:20 -0500 Subject: [PATCH 29/48] - progress update. Signed-off-by: Dave Richer --- .../components/job-lifecycle/job-lifecycle.component.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 07cb778c9..1bfeef189 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -8,6 +8,13 @@ import {isEmpty} from "lodash"; require('./job-lifecycle.styles.scss'); +// Get Bodyshop record +// md_RepairStatus +// All status, array of strings, all statuses available system wide, the order is meaningful. + +// CHECK SORT OF LEGEND + +// show text on bar if text can fit export function JobLifecycleComponent({job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); @@ -192,7 +199,6 @@ export function JobLifecycleComponent({job, ...rest}) { )}> -
From 2a31d740d53c25915808e7096a468a11df921077 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Fri, 26 Jan 2024 08:08:51 -0800 Subject: [PATCH 30/48] Add date disable for load level report. --- client/src/utils/TemplateConstants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index b62ed7664..d37d18145 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2185,6 +2185,7 @@ export const TemplateList = (type, context) => { key: "load_level", //idtype: "vendor", disabled: false, + datedisable:true, rangeFilter: { object: i18n.t("reportcenter.labels.objects.jobs"), field: i18n.t("jobs.fields.date_open"), From 908942ec09c5c492482af670b311b548b1d3c7b1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 26 Jan 2024 08:11:29 -0800 Subject: [PATCH 31/48] IO-2543 Revert Lost Sales for Datedisable --- client/src/utils/TemplateConstants.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index ad614af3b..bb6c23731 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2020,7 +2020,6 @@ export const TemplateList = (type, context) => { key: "lost_sales", //idtype: "vendor", disabled: false, - datedisable: true, rangeFilter: { object: i18n.t("reportcenter.labels.objects.jobs"), field: i18n.t("jobs.fields.date_lost_sale"), From c7a0072f2dceef92e5386eb432602fb423cfbdf2 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 26 Jan 2024 11:19:03 -0500 Subject: [PATCH 32/48] - Revert Hasura Signed-off-by: Dave Richer --- hasura/metadata/tables.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index b9efbb437..17ccc52a9 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -4198,7 +4198,7 @@ interval_sec: 10 num_retries: 0 timeout_sec: 60 - webhook: https://worktest.home.irony.online + webhook_from_env: HASURA_API_URL headers: - name: event-secret value_from_env: EVENT_SECRET From efd1c170339fd527e729ce5a78feb91e57e3e62d Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Fri, 26 Jan 2024 11:19:03 -0500 Subject: [PATCH 33/48] - Revert Hasura Signed-off-by: Dave Richer --- hasura/metadata/tables.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index b9efbb437..17ccc52a9 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -4198,7 +4198,7 @@ interval_sec: 10 num_retries: 0 timeout_sec: 60 - webhook: https://worktest.home.irony.online + webhook_from_env: HASURA_API_URL headers: - name: event-secret value_from_env: EVENT_SECRET From 7503d86c693889c238b3660cdeb32be238e9c9e4 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 26 Jan 2024 08:42:57 -0800 Subject: [PATCH 34/48] IO-2543 Add wrap to space components to maintain limits of card --- .../jobs-admin-delete-intake.component.jsx | 2 +- .../jobs-admin-mark-reexport.component.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx index ec1bd976b..9d95f6e80 100644 --- a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx +++ b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx @@ -52,7 +52,7 @@ export default function JobAdminDeleteIntake({ job }) { return ( <> - +
+
) : ( diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index becddf9eb..7679ffeed 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1231,7 +1231,7 @@ "not_available": "N/A", "previous_status_accumulated_time": "Previous Status Accumulated Time", "title": "Job Lifecycle Component", - "title_durations": "Historical Status Duration's", + "title_durations": "Historical Status Durations", "title_loading": "Loading", "title_transitions": "Transitions" }, diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js index 75b30c54c..16165d001 100644 --- a/server/utils/calculateStatusDuration.js +++ b/server/utils/calculateStatusDuration.js @@ -87,7 +87,6 @@ const calculateStatusDuration = (transitions, statuses) => { const humanReadableTotal = durationToHumanReadable(moment.duration(totalDuration)); - return { summations: _.isArray(statuses) && !_.isEmpty(statuses) ? summations.sort((a, b) => { return statuses.indexOf(a.status) - statuses.indexOf(b.status); diff --git a/server/utils/durationToHumanReadable.js b/server/utils/durationToHumanReadable.js index f13e24c98..e6820c9bf 100644 --- a/server/utils/durationToHumanReadable.js +++ b/server/utils/durationToHumanReadable.js @@ -15,7 +15,7 @@ const durationToHumanReadable = (duration) => { if (days) parts.push(days + ' day' + (days > 1 ? 's' : '')); if (hours) parts.push(hours + ' hour' + (hours > 1 ? 's' : '')); if (minutes) parts.push(minutes + ' minute' + (minutes > 1 ? 's' : '')); - if (!minutes && !hours && !days && !months && !years && seconds) parts.push(seconds + ' second' + (seconds > 1 ? 's' : '')); + if (seconds) parts.push(seconds + ' second' + (seconds > 1 ? 's' : '')); return parts.join(', '); } From 2f83b0baa7c762eb85f2c0f5626028eedd42f5b2 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 26 Jan 2024 20:29:37 -0800 Subject: [PATCH 41/48] Query correction for GET_JOB_BY_PK --- client/src/graphql/jobs.queries.js | 123 ++++++++--------------------- 1 file changed, 32 insertions(+), 91 deletions(-) diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 4741826bf..419171248 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -549,12 +549,12 @@ export const GET_JOB_BY_PK = gql` actual_delivery actual_in adjustment_bottom_line + alt_transport area_of_damage auto_add_ats available_jobs { id } - alt_transport ca_bc_pvrt ca_customer_gst ca_gst_registrant @@ -570,10 +570,13 @@ export const GET_JOB_BY_PK = gql` year } id - scheduledreturn start status + scheduledreturn } + cieca_pfl + cieca_pfo + cieca_pft cieca_ttl class clm_no @@ -638,8 +641,8 @@ export const GET_JOB_BY_PK = gql` ins_city ins_co_id ins_co_nm - ins_ct_ln ins_ct_fn + ins_ct_ln ins_ea ins_ph1 intakechecklist @@ -648,9 +651,11 @@ export const GET_JOB_BY_PK = gql` job_totals joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { act_price + act_price_before_ppc ah_detail_line alt_partm alt_partno + assigned_team billlines(limit: 1, order_by: { bill: { date: desc } }) { actual_cost actual_price @@ -662,8 +667,8 @@ export const GET_JOB_BY_PK = gql` name } } - joblineid id + joblineid quantity } convertedtolbr @@ -686,6 +691,14 @@ export const GET_JOB_BY_PK = gql` notes oem_partno op_code_desc + parts_dispatch_lines(limit: 1, order_by: { accepted_at: desc }) { + accepted_at + id + parts_dispatch { + employeeid + id + } + } part_qty part_type prt_dsmk_m @@ -707,27 +720,27 @@ export const GET_JOB_BY_PK = gql` other_amount_payable owner { id - ownr_fn - ownr_ln - ownr_co_nm - ownr_ea ownr_addr1 ownr_addr2 ownr_city - ownr_st - ownr_zip + ownr_co_nm ownr_ctry + ownr_ea + ownr_fn + ownr_ln ownr_ph1 ownr_ph2 + ownr_st + ownr_zip tax_number } - owner_owing ownerid + owner_owing ownr_addr1 ownr_addr2 - ownr_ctry ownr_city ownr_co_nm + ownr_ctry ownr_ea ownr_fn ownr_ln @@ -778,17 +791,17 @@ export const GET_JOB_BY_PK = gql` rate_mapa rate_mash rate_matd - regie_number referral_source referral_source_extra + regie_number remove_from_ar ro_number scheduled_completion scheduled_delivery scheduled_in selling_dealer - servicing_dealer selling_dealer_contact + servicing_dealer servicing_dealer_contact special_coverage_policy state_tax_rate @@ -807,12 +820,11 @@ export const GET_JOB_BY_PK = gql` towing_payable unit_number updated_at - v_vin + v_color + v_make_desc v_model_yr v_model_desc - v_make_desc - v_color - vehicleid + v_vin vehicle { id jobs { @@ -822,87 +834,16 @@ export const GET_JOB_BY_PK = gql` status } notes - location - tax_part - db_ref - manual_line - prt_dsmk_p - prt_dsmk_m - ioucreated - convertedtolbr - ah_detail_line - act_price_before_ppc - critical - parts_dispatch_lines(limit: 1, order_by: { accepted_at: desc }) { - id - accepted_at - parts_dispatch { - id - employeeid - } - } - assigned_team - billlines(limit: 1, order_by: { bill: { date: desc } }) { - id - quantity - actual_cost - actual_price - joblineid - bill { - id - invoice_number - vendor { - id - name - } - } - } plate_no plate_st v_color v_make_desc - v_model_desc v_model_yr + v_model_desc v_paint_codes v_vin } - payments { - id - jobid - amount - payer - paymentnum - created_at - transactionid - memo - date - type - exportedat - } - cccontracts { - id - status - start - scheduledreturn - agreementnumber - courtesycar { - id - make - model - year - plate - fleetnumber - } - } - cieca_ttl - cieca_pfo - cieca_pfl - cieca_pft - materials - csiinvites { - id - completedon - } + vehicleid voided } } From cb4a6e8774d53c9428438011c75220bff9ab05a8 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 29 Jan 2024 09:05:46 -0800 Subject: [PATCH 42/48] IO-1532 resolve update logic issue for status timings. --- server/graphql-client/queries.js | 12 ++- server/job/job-status-transition.js | 135 +++++++++++++++------------- 2 files changed, 82 insertions(+), 65 deletions(-) diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 2bd83d1ab..fe1c7b6e5 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1989,12 +1989,20 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $ } }`; -exports.INSERT_NEW_TRANSITION = `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, $oldTransitionId: uuid, $duration: numeric) { +exports.INSERT_NEW_TRANSITION = ( + includeOldTransition +) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${ + includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : "" +}) { insert_transitions_one(object: $newTransition) { id } - update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) { + ${ + includeOldTransition + ? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) { affected_rows + }` + : "" } }`; diff --git a/server/job/job-status-transition.js b/server/job/job-status-transition.js index 960041746..d97249c06 100644 --- a/server/job/job-status-transition.js +++ b/server/job/job-status-transition.js @@ -11,82 +11,91 @@ const path = require("path"); const client = require("../graphql-client/graphql-client").client; require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), }); async function StatusTransition(req, res) { - const { - id: jobid, - status: value, - shopid: bodyshopid, - } = req.body.event.data.new; + const { + id: jobid, + status: value, + shopid: bodyshopid, + } = req.body.event.data.new; - // Create record OPEN on new item, enter state - // If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status - // (Timeline) - // Final status is exported, there is no end date as there is no further transition (has no end date) - try { - const {update_transitions} = await client.request( - queries.UPDATE_OLD_TRANSITION, - { - jobid: jobid, - existingTransition: { - end: new Date(), - next_value: value, + // Create record OPEN on new item, enter state + // If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status + // (Timeline) + // Final status is exported, there is no end date as there is no further transition (has no end date) + try { + const { update_transitions } = await client.request( + queries.UPDATE_OLD_TRANSITION, + { + jobid: jobid, + existingTransition: { + end: new Date(), + next_value: value, - //duration - }, - } - ); + //duration + }, + } + ); - let duration = - update_transitions.affected_rows === 0 - ? 0 - : new Date(update_transitions.returning[0].end) - - new Date(update_transitions.returning[0].start); + let duration = + update_transitions.affected_rows === 0 + ? 0 + : new Date(update_transitions.returning[0].end) - + new Date(update_transitions.returning[0].start); - const resp2 = await client.request(queries.INSERT_NEW_TRANSITION, { - oldTransitionId: + const resp2 = await client.request( + queries.INSERT_NEW_TRANSITION(update_transitions.affected_rows > 0), + { + ...(update_transitions.affected_rows > 0 + ? { + oldTransitionId: update_transitions.affected_rows === 0 - ? null - : update_transitions.returning[0].id, - duration, - newTransition: { - bodyshopid: bodyshopid, - jobid: jobid, - start: - update_transitions.affected_rows === 0 - ? new Date() - : update_transitions.returning[0].end, - prev_value: - update_transitions.affected_rows === 0 - ? null - : update_transitions.returning[0].value, - value: value, - type: "status", - }, - }); + ? null + : update_transitions.returning[0].id, + duration, + } + : {}), + newTransition: { + bodyshopid: bodyshopid, + jobid: jobid, + start: + update_transitions.affected_rows === 0 + ? new Date() + : update_transitions.returning[0].end, + prev_value: + update_transitions.affected_rows === 0 + ? null + : update_transitions.returning[0].value, + value: value, + type: "status", + }, + } + ); - //Check to see if there is an existing status transition record. - //Query using Job ID, start is not null, end is null. + logger.log("job-transition-update-result", "DEBUG", null, jobid, resp2); - //If there is no existing record, this is the start of the transition life cycle. - // Create the initial transition record. + //Check to see if there is an existing status transition record. + //Query using Job ID, start is not null, end is null. - //If there is a current status transition record, update it with the end date, duration, and next value. + //If there is no existing record, this is the start of the transition life cycle. + // Create the initial transition record. - res.sendStatus(200); //.json(ret); - } catch (error) { - logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, { - message: error.message, - stack: error.stack, - }); + //If there is a current status transition record, update it with the end date, duration, and next value. - res.status(400).send(JSON.stringify(error)); - } + res.sendStatus(200); //.json(ret); + } catch (error) { + logger.log("job-status-transition-error", "ERROR", req.user?.email, jobid, { + message: error.message, + stack: error.stack, + }); + + res.status(400).send(JSON.stringify(error)); + } } exports.statustransition = StatusTransition; From ae56e27e5f6974fdc742160a66b5aaa7764a8335 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 29 Jan 2024 12:20:36 -0800 Subject: [PATCH 43/48] Update CI for Rome Test branches. --- .circleci/config.yml | 80 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 07f29ff52..a9a73ee39 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -180,8 +180,27 @@ jobs: - aws-s3/sync: from: build to: "s3://rome-online-test/" - - jira/notify + - jira/notify + rome-app-beta-build: + docker: + - image: cimg/node:18.18.2 + resource_class: large + working_directory: ~/repo/client + + steps: + - checkout: + path: ~/repo + - run: + name: Install Dependencies + command: npm i + + - run: npm run build + + - aws-s3/sync: + from: build + to: "s3://rome-online-production-beta/" + - jira/notify test-hasura-migrate: docker: @@ -233,6 +252,49 @@ jobs: to: "s3://imex-online-test/" - jira/notify + test-app-beta-build: + docker: + - image: cimg/node:18.18.2 + resource_class: large + working_directory: ~/repo/client + + steps: + - checkout: + path: ~/repo + + - run: + name: Install Dependencies + command: npm i + + - run: npm run build:test + + - aws-s3/sync: + from: build + to: "s3://imex-online-test-beta/" + - jira/notify + + rome-test-app-beta-build: + docker: + - image: cimg/node:18.18.2 + resource_class: large + working_directory: ~/repo/client + + steps: + - checkout: + path: ~/repo + + - run: + name: Install Dependencies + command: npm i + + - run: npm run build:test + + - aws-s3/sync: + from: build + to: "s3://rome-online-test-beta/" + - jira/notify + + admin-app-build: docker: - image: cimg/node:16.15.0 @@ -274,6 +336,14 @@ workflows: filters: branches: only: master + - app-beta-build: + filters: + branches: + only: master-beta + - rome-app-beta-build: + filters: + branches: + only: rome/master-beta - hasura-migrate: secret: ${HASURA_PROD_SECRET} filters: @@ -296,6 +366,14 @@ workflows: filters: branches: only: test + - rome-test-app-beta-build: + filters: + branches: + only: rome/test-beta + - test-app-beta-build: + filters: + branches: + only: test-beta - test-hasura-migrate: secret: ${HASURA_TEST_SECRET} filters: From 69b4b765017284098838de5816287ec4c0eaef16 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 29 Jan 2024 12:25:04 -0800 Subject: [PATCH 44/48] Comment out beta switch while Rome Beta has not yet started. --- .../components/header/header.component.jsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 356f3a50f..5c0014132 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -13,7 +13,8 @@ import Icon, { FileFilled, //GlobalOutlined, HomeFilled, - ImportOutlined, InfoCircleOutlined, + ImportOutlined, + InfoCircleOutlined, LineChartOutlined, PaperClipOutlined, PhoneOutlined, @@ -26,8 +27,8 @@ import Icon, { UserOutlined, } from "@ant-design/icons"; import { useTreatments } from "@splitsoftware/splitio-react"; -import {Layout, Menu, Switch, Tooltip} from "antd"; -import React, {useEffect, useState} from "react"; +import { Layout, Menu, Switch, Tooltip } from "antd"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { BsKanban } from "react-icons/bs"; import { @@ -52,7 +53,7 @@ import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; -import {handleBeta, setBeta, checkBeta} from "../../utils/handleBeta"; +import { handleBeta, setBeta, checkBeta } from "../../utils/handleBeta"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, @@ -116,7 +117,7 @@ function Header({ setBeta(checked); setBetaSwitch(checked); handleBeta(); - } + }; return ( @@ -443,17 +444,18 @@ function Header({ ))} - - - - Try the new ImEX Online - - - - + { + // + // + // + // Try the new ImEX Online + // + // + // + } ); From 72da0734c846ff5948858948e49a9e3377a115db Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 29 Jan 2024 12:27:52 -0800 Subject: [PATCH 45/48] Resolve CI issue. --- .circleci/config.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index a9a73ee39..b1451e587 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -182,6 +182,26 @@ jobs: to: "s3://rome-online-test/" - jira/notify + app-beta-build: + docker: + - image: cimg/node:18.18.2 + resource_class: large + working_directory: ~/repo/client + + steps: + - checkout: + path: ~/repo + - run: + name: Install Dependencies + command: npm i + + - run: npm run build + + - aws-s3/sync: + from: build + to: "s3://imex-online-beta/" + - jira/notify + rome-app-beta-build: docker: - image: cimg/node:18.18.2 From 62dac2193f53f8c5072aafd2b15ce9d0d7cc41d5 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 29 Jan 2024 12:41:43 -0800 Subject: [PATCH 46/48] Resolve CI warnings for unused components. --- .../components/header/header.component.jsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 5c0014132..091b97dc2 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -14,7 +14,7 @@ import Icon, { //GlobalOutlined, HomeFilled, ImportOutlined, - InfoCircleOutlined, + // InfoCircleOutlined, LineChartOutlined, PaperClipOutlined, PhoneOutlined, @@ -27,8 +27,11 @@ import Icon, { UserOutlined, } from "@ant-design/icons"; import { useTreatments } from "@splitsoftware/splitio-react"; -import { Layout, Menu, Switch, Tooltip } from "antd"; -import React, { useEffect, useState } from "react"; +import { + Layout, + Menu, // Switch, Tooltip +} from "antd"; +import React from "react"; //, { useEffect, useState } import { useTranslation } from "react-i18next"; import { BsKanban } from "react-icons/bs"; import { @@ -53,7 +56,7 @@ import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; -import { handleBeta, setBeta, checkBeta } from "../../utils/handleBeta"; +//import { handleBeta, setBeta, checkBeta } from "../../utils/handleBeta"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, @@ -104,20 +107,20 @@ function Header({ {}, bodyshop && bodyshop.imexshopid ); - const [betaSwitch, setBetaSwitch] = useState(false); + //const [betaSwitch, setBetaSwitch] = useState(false); const { t } = useTranslation(); - useEffect(() => { - const isBeta = checkBeta(); - setBetaSwitch(isBeta); - }, []); + // useEffect(() => { + // const isBeta = checkBeta(); + // setBetaSwitch(isBeta); + // }, []); - const betaSwitchChange = (checked) => { - setBeta(checked); - setBetaSwitch(checked); - handleBeta(); - }; + // const betaSwitchChange = (checked) => { + // setBeta(checked); + // setBetaSwitch(checked); + // handleBeta(); + // }; return ( From 39bba3623ac61fd75ef107bd4a71fe0323815057 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 29 Jan 2024 12:56:14 -0800 Subject: [PATCH 47/48] Resolve broken routes. --- server/routes/payrollRoutes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routes/payrollRoutes.js b/server/routes/payrollRoutes.js index aebd88d02..1666184ec 100644 --- a/server/routes/payrollRoutes.js +++ b/server/routes/payrollRoutes.js @@ -8,8 +8,8 @@ router.use(validateFirebaseIdTokenMiddleware); router.use(withUserGraphQLClientMiddleware); router.post("/calculatelabor", payroll.calculatelabor); -router.post("payall", payroll.payall); -router.post("claimtask", payroll.claimtask); +router.post("/payall", payroll.payall); +router.post("/claimtask", payroll.claimtask); module.exports = router; From d5198edfc09c16e1c45d818ac327ee3047b62365 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 29 Jan 2024 12:59:55 -0800 Subject: [PATCH 48/48] Improve team name label. --- bodyshop_translations.babel | 1108 ++++++++++++++++++++- client/src/translations/en_us/common.json | 7 +- client/src/translations/es/common.json | 3 +- client/src/translations/fr/common.json | 3 +- 4 files changed, 1115 insertions(+), 6 deletions(-) diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 7aee50533..da74229ae 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1,4 +1,4 @@ - +