Compare commits

...

103 Commits

Author SHA1 Message Date
Allan Carr
33ec18986d IO-2556 CC Sort Order
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-20 13:10:57 -08:00
Allan Carr
2427c14b7b Merged in release/2024-02-16 (pull request #1296)
IO-2631 Correct Import Statement for moment

Approved-by: Dave Richer
2024-02-16 21:37:49 +00:00
Allan Carr
66655d449e Merged in feature/IO-2631-Update-Schedule-Completion-on-Import (pull request #1294)
IO-2631 Correct Import Statement for moment

Approved-by: Dave Richer
2024-02-16 20:33:05 +00:00
Allan Carr
845a84c4c8 IO-2631 Correct Import Statement for moment 2024-02-16 12:30:12 -08:00
Dave Richer
0ff5ea3d59 Merged in release/2024-02-16 (pull request #1291)
Release/2024 02 16

Approved-by: Patrick Fic
2024-02-16 20:21:35 +00:00
Allan Carr
09492e647e Merged in feature/IO-2631-Update-Schedule-Completion-on-Import (pull request #1292)
IO-2631 Correct for Business Days

Approved-by: Dave Richer
2024-02-16 19:19:08 +00:00
Allan Carr
2d6594cc73 IO-2631 Correct for Business Days 2024-02-16 11:08:59 -08:00
Allan Carr
601fdbba39 Merged in feature/IO-2637-Add-Scoreboard-Date (pull request #1287)
IO-2637 Correct for Timezone offset

Approved-by: Dave Richer
2024-02-16 17:23:05 +00:00
Allan Carr
830adc4ef3 IO-2637 Correct for Timezone offset 2024-02-16 08:52:33 -08:00
Dave Richer
3f216195ca Merge branch 'feature/IO-2456-Report-Filters-From-Master' into release/2024-02-16 2024-02-15 21:03:30 -05:00
Dave Richer
bcb8de0937 - Fix issues with labels on sorters
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 21:03:04 -05:00
Dave Richer
4c07632ee6 Merged in feature/IO-2456-Report-Filters-From-Master (pull request #1283)
- Report Center Filters Version 1 retargeted to Master

Approved-by: Patrick Fic
2024-02-15 17:32:38 +00:00
Dave Richer
767c219af8 - Remove additional console.log
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 11:59:56 -05:00
Dave Richer
cfc301570e - Remove redundant CSS
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 11:57:27 -05:00
Allan Carr
fb718f9f37 Merged in feature/IO-2631-Update-Schedule-Completion-on-Import (pull request #1284)
IO-2631 Update Scheduled Completion on Supp

Approved-by: Dave Richer
2024-02-15 16:53:41 +00:00
Dave Richer
cafc0e5628 - Update GUI and provide loading state
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 11:49:02 -05:00
Dave Richer
a635725839 - Remove console.log statements
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-15 10:58:19 -05:00
Allan Carr
eb8e9b10ef IO-2631 Update Scheduled Completion on Supp 2024-02-14 16:47:25 -08:00
Dave Richer
2584f7129c - Report Center Filters Version 1 retargeted to Master
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-14 14:31:35 -05:00
Allan Carr
6c996037d6 Merged in release/2024-02-02 (pull request #1274)
Release/2024 02 02

Approved-by: Patrick Fic
2024-02-09 18:47:24 +00:00
Allan Carr
f421b45222 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1272)
IO-2630 Adjust Query to match Tags
2024-02-09 17:54:46 +00:00
Allan Carr
30cf46a158 IO-2630 Adjust Query to match Tags 2024-02-09 09:54:22 -08:00
Allan Carr
8d37c54f89 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1269)
Feature/IO-2630 Parts Queue Mods

Approved-by: Dave Richer
2024-02-09 16:14:51 +00:00
Allan Carr
f9b9f39418 IO-2630 Add in Drawer for Parts Queue 2024-02-08 17:18:14 -08:00
Allan Carr
05a5df789b IO-2030 Change & Add Columns, Add Sorters and Filters 2024-02-08 13:22:00 -08:00
Allan Carr
bed87eda97 Merged in feature/IO-2626-CSI-Pages (pull request #1267)
IO-2626 Adjust Image Prop on customer page
2024-02-08 17:58:21 +00:00
Allan Carr
67008c35b8 IO-2626 Adjust Image Prop on customer page 2024-02-08 09:57:39 -08:00
Patrick Fic
9b95e40a13 Merge branch 'release/2024-02-02' of bitbucket.org:snaptsoft/bodyshop into release/2024-02-02 2024-02-06 10:05:26 -08:00
Patrick Fic
e3a998b6f8 Merge branch 'feature/IO-2626-CSI-Pages' into release/2024-02-02 2024-02-06 10:05:11 -08:00
Patrick Fic
3c7ede0155 IO-2626 Prevent crisp from loading on anon CSI page. 2024-02-06 10:04:59 -08:00
Allan Carr
82c7aa1347 Merged in feature/IO-2626-CSI-Pages (pull request #1265)
IO-2626 Resource Class for Test
2024-02-06 17:37:46 +00:00
Allan Carr
4ccd912363 Merge branch 'feature/IO-2626-CSI-Pages' of bitbucket.org:snaptsoft/bodyshop into feature/IO-2626-CSI-Pages 2024-02-06 09:36:16 -08:00
Patrick Fic
c0525842fd Merge branch 'feature/IO-2626-CSI-Pages' into release/2024-02-02 2024-02-06 09:29:39 -08:00
Patrick Fic
dd5ca5d233 Add resource class to test build as well. 2024-02-06 09:29:22 -08:00
Allan Carr
616a4b04a0 IO-2626 Resource Class for Test 2024-02-06 09:10:32 -08:00
Allan Carr
9a34640c88 Merged in feature/IO-2626-CSI-Pages (pull request #1263)
IO-2626 CICD Resource Size Change
2024-02-06 16:55:55 +00:00
Allan Carr
3110be4703 IO-2626 CICD Resource Size Change 2024-02-06 08:52:51 -08:00
Allan Carr
971c8d41ff Merged in feature/IO-2626-CSI-Pages (pull request #1253)
IO-2626 CSI Pages
2024-02-06 16:17:50 +00:00
Allan Carr
7c303a5154 IO-2626 Change Server Variable Name for response 2024-02-05 20:06:28 -08:00
Allan Carr
9383b37a41 IO-2626 Modify seachparm and fix linking 2024-02-05 20:03:17 -08:00
Allan Carr
205d507097 IO-2626 Update Translations 2024-02-02 22:50:40 -08:00
Allan Carr
97a1bd66d1 IO-2626 Correct Sorting, Linking, Pagination and Update Response Container 2024-02-02 22:45:06 -08:00
Allan Carr
0d1ff6390c IO-2626 Correct Error Page footer 2024-02-02 12:35:18 -08:00
Allan Carr
830d2c87d2 IO-2626 CSI Pages
Move to Server side initial commit
2024-02-02 11:55:57 -08:00
Allan Carr
da98e7b886 Merged in feature/IO-2624-Federal-Tax-Exempt-Destructure (pull request #1251)
IO-2624 federal_tax_exempt destructure

Approved-by: Dave Richer
2024-02-02 17:18:24 +00:00
Allan Carr
a74a9ba5a1 IO-2624 federal_tax_exempt destructure 2024-01-31 09:59:56 -08:00
Patrick Fic
b8a3081488 Merged in feature/Sentry-Improvements (pull request #1247)
Change tracing targets.
2024-01-31 01:09:03 +00:00
Patrick Fic
c8fc1b0f68 Merged in feature/Sentry-Improvements (pull request #1246)
Change tracing targets.
2024-01-31 01:08:37 +00:00
Patrick Fic
da1ddb874f Change tracing targets. 2024-01-30 16:58:11 -08:00
Patrick Fic
771f9ceaa8 Merged in feature/Sentry-Improvements (pull request #1242)
Feature/Sentry Improvements
2024-01-30 21:45:22 +00:00
Patrick Fic
9b61da5c62 Merged in feature/Sentry-Improvements (pull request #1239)
Feature/Sentry Improvements
2024-01-30 21:42:33 +00:00
Patrick Fic
7daf7540b3 Resolve Typo in CI. 2024-01-30 13:32:58 -08:00
Patrick Fic
ce6940629d Exclude source map upload in CI. 2024-01-30 13:25:48 -08:00
Patrick Fic
b706b96d32 Remove sentry test button. 2024-01-30 13:14:27 -08:00
Patrick Fic
2427bc72f2 Updated CI. 2024-01-30 12:53:07 -08:00
Patrick Fic
8bc1a9d9ee Initial sentry improvements to deploy and verify against test. 2024-01-30 12:47:12 -08:00
Patrick Fic
ba30225ba1 Merged in release/2024-01-29 (pull request #1230)
IO-1532 resolve update logic issue for status timings.
2024-01-29 17:06:31 +00:00
Patrick Fic
cb4a6e8774 IO-1532 resolve update logic issue for status timings. 2024-01-29 09:05:46 -08:00
Dave Richer
23f640028d Merged in release/2024-01-26 (pull request #1227)
Release/2024 01 26
2024-01-27 02:25:22 +00:00
Dave Richer
3e5e6263fe Merged in feature/IO-1532-Tracking-Department-Cycle-Times (pull request #1223)
- updates from lifecyle component.
2024-01-26 23:07:02 +00:00
Dave Richer
48cef3e188 - updates from lifecyle component.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 18:06:14 -05:00
Dave Richer
8c0d6b2f6b Merged in feature/IO-1532-Tracking-Department-Cycle-Times (pull request #1221)
- Fix use hook
2024-01-26 21:34:17 +00:00
Dave Richer
22ee8acd0d - Fix use hook
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 16:33:40 -05:00
Dave Richer
afdcfb7bf6 Merge branch 'feature/IO-1532-Tracking-Department-Cycle-Times' into release/2024-01-26 2024-01-26 16:14:01 -05:00
Dave Richer
c1f6d06128 - Finish department cycle times.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 16:10:24 -05:00
Dave Richer
120a8a4576 - Finish department cycle times.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 16:09:46 -05:00
Dave Richer
89224e871c - Progress Update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 12:57:06 -05:00
Allan Carr
fa0d472fb6 Merged in feature/IO-2543-AR-Aging (pull request #1216)
IO-2543 Add wrap to space components to maintain limits of card

Approved-by: Dave Richer
2024-01-26 16:52:59 +00:00
Dave Richer
b0f4ad7e4f - Progress Update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 11:48:43 -05:00
Allan Carr
7503d86c69 IO-2543 Add wrap to space components to maintain limits of card 2024-01-26 08:42:57 -08:00
Dave Richer
efd1c17033 - Revert Hasura
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 08:21:43 -08:00
Dave Richer
c7a0072f2d - Revert Hasura
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-26 11:19:03 -05:00
Allan Carr
908942ec09 IO-2543 Revert Lost Sales for Datedisable 2024-01-26 08:11:29 -08:00
Allan Carr
e4d3b53349 Merged in feature/IO-2543-AR-Aging (pull request #1215)
IO-2543 Revert Lost Sales for Datedisable

Approved-by: Dave Richer
2024-01-26 16:10:08 +00:00
Dave Richer
03ce5458b5 Merge branch 'release/2024-01-26' into feature/IO-1532-Tracking-Department-Cycle-Times 2024-01-25 14:00:26 -05:00
Dave Richer
61c03ee206 Merge branch 'release/2024-01-26' into feature/IO-1532-Tracking-Department-Cycle-Times 2024-01-25 13:56:25 -05:00
Dave Richer
0e4f5b8b2a - progress update.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-25 13:35:20 -05:00
Allan Carr
b5d4944ad8 Merged in feature/IO-2543-AR-Aging (pull request #1210)
IO-2543 AR Aging

Approved-by: Dave Richer
Approved-by: Patrick Fic
2024-01-25 18:33:20 +00:00
Patrick Fic
50f84d40e1 Allow negative balance for AR. 2024-01-25 10:30:03 -08:00
Dave Richer
a394d6b37e - Major Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-25 12:38:32 -05:00
Dave Richer
f8408908b2 - Major Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 21:40:05 -05:00
Allan Carr
eb8519dc1d IO-2543 Move queries to jobs.queries.js and correct layout 2024-01-24 18:37:41 -08:00
Allan Carr
36dd97394f IO-2543 Adjust for having no dates 2024-01-24 16:15:32 -08:00
Allan Carr
03d4e4dcd1 IO-2543 AR Aging 2024-01-24 16:01:48 -08:00
Dave Richer
6489a8666f - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 18:39:59 -05:00
Dave Richer
5ea64ed805 - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 17:18:43 -05:00
Dave Richer
d740446ccb - Progress
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-24 10:07:07 -05:00
Dave Richer
d0a2bb7da0 - human readable dates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 12:58:57 -05:00
Dave Richer
5de4ef5d83 - human readable dates
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 12:54:38 -05:00
Dave Richer
f59bdf9030 - Progress Update
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 12:35:58 -05:00
Dave Richer
cfe0727447 - Rough in front end / backend
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 10:20:26 -05:00
Dave Richer
09d112350a - Rough in front end / backend
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 01:37:11 -05:00
Dave Richer
52f8eabd2b - Finish cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-23 00:02:18 -05:00
Dave Richer
a162b275a3 - Finish cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 23:11:10 -05:00
Dave Richer
2e7232bb65 - Finish cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 23:00:31 -05:00
Dave Richer
82dc9e1c56 - Finish cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 19:07:16 -05:00
Dave Richer
272a3f579a - Minor cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 18:53:57 -05:00
Dave Richer
ff1ceb20cb - Minor cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 18:39:27 -05:00
Dave Richer
343179d4fe - Minor cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 18:05:35 -05:00
Dave Richer
eabbc2211b - Minor cleanup
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 17:21:46 -05:00
Dave Richer
7f587680ca - Scaffolding.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 16:38:26 -05:00
Patrick Fic
d61ed03ef1 Merge in EULA changes & update to AR Tracking schema. 2024-01-22 11:52:42 -08:00
Dave Richer
166efdc877 progressUpdate
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-01-22 14:25:21 -05:00
120 changed files with 54019 additions and 3289 deletions

View File

@@ -42,31 +42,22 @@ jobs:
app-build:
docker:
- image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
command: npm i
- run: yarn run build
- run: npm run build
- aws-s3/sync:
from: build
to: "s3://imex-online-production/"
arguments: "--exclude '*.map'"
- jira/notify
test-hasura-migrate:
@@ -92,31 +83,22 @@ jobs:
test-app-build:
docker:
- image: cimg/node:16.15.0
resource_class: large
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
command: npm i
- run: yarn run build:test
- run: npm run build:test
- aws-s3/sync:
from: build
to: "s3://imex-online-test/"
arguments: "--exclude '*.map'"
- jira/notify
admin-app-build:
@@ -177,4 +159,4 @@ workflows:
#- admin-app-build:
#filters:
#branches:
#only: master
#only: master

View File

@@ -0,0 +1,120 @@
# Filters and Sorters
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports.
## High level Schema Overview
```javascript
const schema = {
"filters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more filters
],
"sorters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more sorters
],
"dates": {
// This is not yet implemented and will be added in a future release
}
}
```
## Filters
Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
A note on special notation used in the `name` field.
### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",`
This will produce a where clause at the `joblines` level of the graphQL query,
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query.
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
## Known Caveats
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The `dates` object is not yet implemented and will be added in a future release.
- The type object must be 'string' or 'number' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- Do not add the ability to filter things that are already filtered as part of the original query, this would be redundant and could cause issues.
- Do not add the ability to filter on things like FK constraints, must like the above example.
## Sorters
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level.
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters.

View File

@@ -1,4 +1,4 @@
GENERATE_SOURCEMAP=false
GENERATE_SOURCEMAP=true
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
REACT_APP_GA_CODE=231103507

3
client/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Sentry Config File
.sentryclirc

View File

@@ -1,25 +1,25 @@
// craco.config.js
const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less");
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
//const SentryWebpackPlugin = require("@sentry/webpack-plugin");
module.exports = {
plugins: [
{
plugin: SentryWebpackPlugin,
options: {
// sentry-cli configuration
authToken:
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
org: "snapt-software",
project: "imexonline",
release: process.env.REACT_APP_GIT_SHA,
// {
// plugin: SentryWebpackPlugin,
// options: {
// // sentry-cli configuration
// authToken:
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
// org: "snapt-software",
// project: "imexonline",
// release: process.env.REACT_APP_GIT_SHA,
// webpack-specific configuration
include: ".",
ignore: ["node_modules", "webpack.config.js"],
},
},
// // webpack-specific configuration
// include: ".",
// ignore: ["node_modules", "webpack.config.js"],
// },
// },
{
plugin: CracoLessPlugin,
options: {

636
client/package-lock.json generated
View File

@@ -13,12 +13,14 @@
"@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.40.0",
"@sentry/cli": "^2.27.0",
"@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.40.0",
"@splitsoftware/splitio-react": "^1.8.1",
"@tanem/react-nprogress": "^5.0.8",
"antd": "^4.24.8",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.3.4",
"craco-less": "^2.0.0",
"dinero.js": "^1.9.1",
@@ -4214,40 +4216,195 @@
"integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==",
"license": "MIT"
},
"node_modules/@sentry/browser": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.40.0.tgz",
"integrity": "sha512-07rZ+cTcpmYB1r84/oZtmSPJJvLCxW8yIh/5s4MdKRyZpqIDKhOz6cCS/4j+l1V+MeLcNLZBjFtNdKA2eocTpg==",
"license": "MIT",
"node_modules/@sentry-internal/feedback": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.99.0.tgz",
"integrity": "sha512-exIO1o+bE0MW4z30FxC0cYzJ4ZHSMlDPMHCBDPzU+MWGQc/fb8s58QUrx5Dnm6HTh9G3H+YlroCxIo9u0GSwGQ==",
"dependencies": {
"@sentry/core": "7.40.0",
"@sentry/replay": "7.40.0",
"@sentry/types": "7.40.0",
"@sentry/utils": "7.40.0",
"tslib": "^1.9.3"
"@sentry/core": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry-internal/feedback/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
"node_modules/@sentry-internal/feedback/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/feedback/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.99.0.tgz",
"integrity": "sha512-PoIkfusToDq0snfl2M6HJx/1KJYtXxYhQplrn11kYadO04SdG0XGXf4h7wBTMEQ7LDEAtQyvsOu4nEQtTO3YjQ==",
"dependencies": {
"@sentry/core": "7.99.0",
"@sentry/replay": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.99.0.tgz",
"integrity": "sha512-z3JQhHjoM1KdM20qrHwRClKJrNLr2CcKtCluq7xevLtXHJWNAQQbafnWD+Aoj85EWXBzKt9yJMv2ltcXJ+at+w==",
"dependencies": {
"@sentry/core": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/tracing/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.99.0.tgz",
"integrity": "sha512-bgfoUv3wkwwLgN5YUOe0ibB3y268ZCnamZh6nLFqnY/UBKC1+FXWFdvzVON/XKUm62LF8wlpCybOf08ebNj2yg==",
"dependencies": {
"@sentry-internal/feedback": "7.99.0",
"@sentry-internal/replay-canvas": "7.99.0",
"@sentry-internal/tracing": "7.99.0",
"@sentry/core": "7.99.0",
"@sentry/replay": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/cli": {
"version": "1.74.6",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.74.6.tgz",
"integrity": "sha512-pJ7JJgozyjKZSTjOGi86chIngZMLUlYt2HOog+OJn+WGvqEkVymu8m462j1DiXAnex9NspB4zLLNuZ/R6rTQHg==",
"dev": true,
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.27.0.tgz",
"integrity": "sha512-pc0opd71W8lGhYvmB1keQtJkarxzCS9f9ErKYv6TfXOOX6drvwkyA6vD/6xEnpzyvqGAuGRU4T4sEeLD3irwUQ==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"https-proxy-agent": "^5.0.0",
"mkdirp": "^0.5.5",
"node-fetch": "^2.6.7",
"npmlog": "^4.1.2",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0",
"which": "^2.0.2"
@@ -4256,7 +4413,124 @@
"sentry-cli": "bin/sentry-cli"
},
"engines": {
"node": ">= 8"
"node": ">= 10"
},
"optionalDependencies": {
"@sentry/cli-darwin": "2.27.0",
"@sentry/cli-linux-arm": "2.27.0",
"@sentry/cli-linux-arm64": "2.27.0",
"@sentry/cli-linux-i686": "2.27.0",
"@sentry/cli-linux-x64": "2.27.0",
"@sentry/cli-win32-i686": "2.27.0",
"@sentry/cli-win32-x64": "2.27.0"
}
},
"node_modules/@sentry/cli-darwin": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.27.0.tgz",
"integrity": "sha512-/DOZlN5rK19g7YP2OaVNauQhUrRfJ88RDr6qURFiqdxYHDc3isPFGHZJmeZBTwOnDDepyZb4XLaOyfwvAOxHig==",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.27.0.tgz",
"integrity": "sha512-JmMQ9zgFhkZUEN5WIYuJisu4Jif/ThRHDjbsbXBRbUkkgRn88hgUfg299djMvlZZxjpl3K9AEua+1TIUeQd0Sg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-arm64": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.27.0.tgz",
"integrity": "sha512-f+zuB9XGfB8pNamNgSDhqsavuLuzi6saZxbr3uQf30bA5AESI5hspOd1zPcidOORCVZxiPzQe3+T7avBI1XLuw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-i686": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.27.0.tgz",
"integrity": "sha512-/4eyz7jnYp20mZqNtpvCEBkxFW0nEjEZRo2BiASQ5/7K8CmoJRe1vhpDA0WOfzi1zTFIfpdE1/RZm2CjHS6DHQ==",
"cpu": [
"x86",
"ia32"
],
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-linux-x64": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.27.0.tgz",
"integrity": "sha512-ptu7wXecnYssihzHlxEOaqbFHWmNEfbepBKGXTdWK2kC+D51+7yHsR9xRdThwVID1bisFgjAveKmBQjmKuXjHQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux",
"freebsd"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-i686": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.27.0.tgz",
"integrity": "sha512-Db4/xmdE5qV4Aq7Yc8vRw22Y46JJdGMdsMsl5jIf0GVSQPgO23O/2uTiDGpPOdeq91K9EtvpH1zQfDLIfLMaXw==",
"cpu": [
"x86",
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/cli-win32-x64": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.27.0.tgz",
"integrity": "sha512-q7y/BH4iGfs0TD5PXh2Q8oqnTbOIufoT1NWJcKqvZcOiqCLK3PNUiq7xUeX1PMTrFYAh3Bm6EekOnMavqvbGmg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@sentry/core": {
@@ -4280,16 +4554,15 @@
"license": "0BSD"
},
"node_modules/@sentry/react": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.40.0.tgz",
"integrity": "sha512-7yYagpOCdsXnVTtLL8Y7wAf2xXgsk2ncuju3O/G4kEckkLewZWmQeoknOSGFlAgVdGNhTaXc2WGzgOiBMOkhug==",
"license": "MIT",
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.99.0.tgz",
"integrity": "sha512-RtHwgzMHJhzJfSQpVG0SDPQYMTGDX3Q37/YWI59S4ALMbSW4/F6n/eQAvGVYZKbh2UCSqgFuRWaXOYkSZT17wA==",
"dependencies": {
"@sentry/browser": "7.40.0",
"@sentry/types": "7.40.0",
"@sentry/utils": "7.40.0",
"hoist-non-react-statics": "^3.3.2",
"tslib": "^1.9.3"
"@sentry/browser": "7.99.0",
"@sentry/core": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0",
"hoist-non-react-statics": "^3.3.2"
},
"engines": {
"node": ">=8"
@@ -4298,26 +4571,82 @@
"react": "15.x || 16.x || 17.x || 18.x"
}
},
"node_modules/@sentry/react/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
"node_modules/@sentry/react/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/react/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/react/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.40.0.tgz",
"integrity": "sha512-Y9Kvo9jKouUdrHQhHVv5SmWZClF5o7BFI6oVpLlv4zXORPQlyoZONM/9sxiMvvH73alDSpxzCoxyhlypAOH4ww==",
"license": "MIT",
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.99.0.tgz",
"integrity": "sha512-gyN/I2WpQrLAZDT+rScB/0jnFL2knEVBo8U8/OVt8gNP20Pq8T/rDZKO/TG0cBfvULDUbJj2P4CJryn2p/O2rA==",
"dependencies": {
"@sentry/core": "7.40.0",
"@sentry/types": "7.40.0",
"@sentry/utils": "7.40.0"
"@sentry-internal/tracing": "7.99.0",
"@sentry/core": "7.99.0",
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@sentry/replay/node_modules/@sentry/core": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz",
"integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==",
"dependencies": {
"@sentry/types": "7.99.0",
"@sentry/utils": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay/node_modules/@sentry/types": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz",
"integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay/node_modules/@sentry/utils": {
"version": "7.99.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz",
"integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==",
"dependencies": {
"@sentry/types": "7.99.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/tracing": {
"version": "7.40.0",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.40.0.tgz",
@@ -4381,6 +4710,27 @@
"node": ">= 8"
}
},
"node_modules/@sentry/webpack-plugin/node_modules/@sentry/cli": {
"version": "1.77.3",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.77.3.tgz",
"integrity": "sha512-c3eDqcDRmy4TFz2bFU5Y6QatlpoBPPa8cxBooaS4aMQpnIdLYPF1xhyyiW0LQlDUNc3rRjNF7oN5qKoaRoMTQQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"https-proxy-agent": "^5.0.0",
"mkdirp": "^0.5.5",
"node-fetch": "^2.6.7",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0",
"which": "^2.0.2"
},
"bin": {
"sentry-cli": "bin/sentry-cli"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.24.51",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
@@ -6112,12 +6462,21 @@
"@apollo/client": "^3.0.0"
}
},
"node_modules/aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true,
"license": "ISC"
"node_modules/apollo-link-sentry": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-3.3.0.tgz",
"integrity": "sha512-wLffWmo5sRw3rHN1Ck6azM0oxObvtaBBf3AC8cLX4SxhyjmkRIagGDji6CFkyAhxupPz0b9/H1u4Ocx+63lNug==",
"dependencies": {
"deepmerge": "^4.2.2",
"dot-prop": "^6.0.0",
"tslib": "^2.0.3",
"zen-observable-ts": "^1.2.5"
},
"peerDependencies": {
"@apollo/client": "^3.2.3",
"@sentry/browser": "^7.41.0",
"graphql": "15 - 16"
}
},
"node_modules/arch": {
"version": "2.2.0",
@@ -6140,17 +6499,6 @@
],
"license": "MIT"
},
"node_modules/are-we-there-yet": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
"integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
"dev": true,
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -7571,16 +7919,6 @@
"node": ">=4"
}
},
"node_modules/code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/collect-v8-coverage": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
@@ -7743,13 +8081,6 @@
"node": ">=0.8"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"dev": true,
"license": "ISC"
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -8911,13 +9242,6 @@
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
@@ -9218,6 +9542,28 @@
"tslib": "^2.0.3"
}
},
"node_modules/dot-prop": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
"dependencies": {
"is-obj": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dot-prop/node_modules/is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"engines": {
"node": ">=8"
}
},
"node_modules/dotenv": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
@@ -10464,6 +10810,8 @@
},
"node_modules/eventemitter2": {
"version": "6.4.7",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
"integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
"dev": true,
"license": "MIT"
},
@@ -11283,74 +11631,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==",
"dev": true,
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
}
},
"node_modules/gauge/node_modules/ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge/node_modules/is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"number-is-nan": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge/node_modules/string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
"dev": true,
"license": "MIT",
"dependencies": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
"strip-ansi": "^3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gauge/node_modules/strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^2.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -11753,13 +12033,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"dev": true,
"license": "ISC"
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -15744,19 +16017,6 @@
"node": ">=8"
}
},
"node_modules/npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"dev": true,
"license": "ISC",
"dependencies": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -15769,16 +16029,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/nwsapi": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz",
@@ -17794,7 +18044,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -20526,13 +20775,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true,
"license": "ISC"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -23067,16 +23309,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wildcard": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",

View File

@@ -9,12 +9,14 @@
"@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^7.40.0",
"@sentry/cli": "^2.27.0",
"@sentry/react": "^7.99.0",
"@sentry/tracing": "^7.40.0",
"@splitsoftware/splitio-react": "^1.8.1",
"@tanem/react-nprogress": "^5.0.8",
"antd": "^4.24.8",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.3.4",
"craco-less": "^2.0.0",
"dinero.js": "^1.9.1",
@@ -88,13 +90,14 @@
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && npm run sentry:sourcemaps",
"build:test": "env-cmd -f .env.test npm run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
"test": "cypress open",
"eject": "react-scripts eject",
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
},
"eslintConfig": {
"extends": [

View File

@@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
moment.locale("en-US");
@@ -18,7 +19,7 @@ export const factory = SplitSdk({
},
});
export default function AppContainer() {
function AppContainer() {
const { t } = useTranslation();
return (
@@ -42,3 +43,5 @@ export default function AppContainer() {
</ApolloProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -94,6 +94,7 @@ function BillEnterModalContainer({
location,
outstanding_returns,
inventory,
federal_tax_exempt,
...remainingValues
} = values;

View File

@@ -5,6 +5,7 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import ConfigFormComponents from "../config-form-components/config-form-components.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
@@ -44,6 +45,13 @@ export default function CsiResponseFormContainer() {
readOnly
componentList={data.csi_by_pk.csiquestion.config}
/>
{data.csi_by_pk.validuntil ? (
<>
{t("csi.fields.validuntil")}
{": "}
<DateFormatter>{data.csi_by_pk.validuntil}</DateFormatter>
</>
) : null}
</Form>
</Card>
);

View File

@@ -5,9 +5,11 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import {pageLimit} from "../../utils/config";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
export default function CsiResponseListPaginated({
refetch,
@@ -16,23 +18,23 @@ export default function CsiResponseListPaginated({
total,
}) {
const search = queryString.parse(useLocation().search);
const { responseid, page, sortcolumn, sortorder } = search;
const { responseid } = search;
const history = useHistory();
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
page: "",
});
const { t } = useTranslation();
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
width: "8%",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}>
{record.job.ro_number || t("general.labels.na")}
@@ -41,15 +43,18 @@ export default function CsiResponseListPaginated({
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
width: "25%",
sortOrder: sortcolumn === "owner" && sortorder,
dataIndex: "owner_name",
key: "owner_name",
sorter: (a, b) =>
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
sortOrder:
state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
render: (text, record) => {
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
return record.job.ownerid ? (
<Link to={"/manage/owners/" + record.job.ownerid}>
<OwnerNameDisplay ownerObject={record.job} />
</Link>
) : (
@@ -64,9 +69,9 @@ export default function CsiResponseListPaginated({
dataIndex: "completedon",
key: "completedon",
ellipsis: true,
sorter: (a, b) => a.completedon - b.completedon,
width: "25%",
sortOrder: sortcolumn === "completedon" && sortorder,
sorter: (a, b) => dateSort(a.completedon, b.completedon),
sortOrder:
state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order,
render: (text, record) => {
return record.completedon ? (
<DateFormatter>{record.completedon}</DateFormatter>
@@ -76,11 +81,12 @@ export default function CsiResponseListPaginated({
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
search.page = pagination.current;
search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order;
history.push({ search: queryString.stringify(search) });
setState({
...state,
filteredInfo: filters,
sortedInfo: sorter,
page: pagination.current,
});
};
const handleOnRowClick = (record) => {
@@ -108,7 +114,7 @@ export default function CsiResponseListPaginated({
pagination={{
position: "top",
pageSize: pageLimit,
current: parseInt(page || 1),
current: parseInt(state.page || 1),
total: total,
}}
columns={columns}
@@ -122,13 +128,6 @@ export default function CsiResponseListPaginated({
selectedRowKeys: [responseid],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
};
}}
/>
</Card>
);

View File

@@ -2,7 +2,7 @@ import { Button, Col, Collapse, Result, Row, Space } from "antd";
import React from "react";
import { withTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import * as Sentry from "@sentry/react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
@@ -138,7 +138,6 @@ class ErrorBoundary extends React.Component {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(ErrorBoundary));
export default Sentry.withErrorBoundary(
connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ErrorBoundary))
);

View File

@@ -0,0 +1,246 @@
import React, {useCallback, useEffect, useState} from 'react';
import moment from "moment";
import axios from 'axios';
import {Badge, Card, Space, Table, Tag} from 'antd';
import {gql, useQuery} from "@apollo/client";
import {DateTimeFormatterFunction} from "../../utils/DateFormatter";
import {isEmpty} from "lodash";
import {useTranslation} from "react-i18next";
require('./job-lifecycle.styles.scss');
// show text on bar if text can fit
export function JobLifecycleComponent({job, statuses, ...rest}) {
const [loading, setLoading] = useState(true);
const [lifecycleData, setLifecycleData] = useState(null);
const {t} = useTranslation(); // 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'
});
/**
* Gets the lifecycle data for the job.
* @returns {Promise<void>}
*/
const getLifecycleData = useCallback(async () => {
if (job && job.id && statuses && statuses.statuses) {
try {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: job.id,
statuses: statuses.statuses
});
const data = response.data.transition[job.id];
setLifecycleData(data);
} catch (err) {
console.error(`${t('job_lifecycle.errors.fetch')}: ${err.message}`);
} finally {
setLoading(false);
}
}
}, [job, statuses, t]);
useEffect(() => {
if (!data) return;
setTimeout(() => {
getLifecycleData().catch(err => console.error(`${t('job_lifecycle.errors.fetch')}: ${err.message}`));
}, 500);
}, [data, getLifecycleData, t]);
const columns = [
{
title: t('job_lifecycle.columns.value'),
dataIndex: 'value',
key: 'value',
},
{
title: t('job_lifecycle.columns.start'),
dataIndex: 'start',
key: 'start',
render: (text) => DateTimeFormatterFunction(text),
sorter: (a, b) => moment(a.start).unix() - moment(b.start).unix(),
},
{
title: t('job_lifecycle.columns.relative_start'),
dataIndex: 'start_readable',
key: 'start_readable',
},
{
title: t('job_lifecycle.columns.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) ? t('job_lifecycle.content.not_available') : DateTimeFormatterFunction(text)
},
{
title: t('job_lifecycle.columns.relative_end'),
dataIndex: 'end_readable',
key: 'end_readable',
},
{
title: t('job_lifecycle.columns.duration'),
dataIndex: 'duration_readable',
key: 'duration_readable',
sorter: (a, b) => a.duration - b.duration,
},
];
return (
<Card loading={loading} title={t('job_lifecycle.content.title')}>
{!loading ? (
lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? (
<Space direction='vertical' style={{width: '100%'}}>
<Card
type='inner'
title={(
<Space direction='horizontal' size='small'>
<Badge status='processing' count={lifecycleData.durations.totalStatuses}/>
{t('job_lifecycle.content.title_durations')}
</Space>
)}
style={{width: '100%'}}
>
<div id="bar-container" style={{
display: 'flex',
width: '100%',
height: '100px',
textAlign: 'center',
borderRadius: '5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#f0f2f5',
margin: 0,
padding: 0
}}>
{lifecycleData.durations.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div key={key.status} style={{
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
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,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ?
<>
<div>{key.roundedPercentage}</div>
<div style={{
backgroundColor: '#f0f2f5',
borderRadius: '5px',
paddingRight: '2px',
paddingLeft: '2px',
fontSize: '0.8rem',
}}>
{key.status}
</div>
</>
: null}
</div>
);
})}
</div>
<Card type='inner' title={t('job_lifecycle.content.legend_title')}
style={{marginTop: '10px'}}>
<div>
{lifecycleData.durations.summations.map((key) => (
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: '#f0f2f5',
color: '#000',
padding: '4px',
textAlign: 'center'
}}>
{key.status} ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
{(lifecycleData?.durations?.humanReadableTotal) ||
(lifecycleData.lifecycle[0] && lifecycleData.lifecycle[0].value && lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable) ?
<Card style={{marginTop: '10px'}}>
<ul>
{lifecycleData.durations && lifecycleData.durations.humanReadableTotal &&
<li>
<span
style={{fontWeight: 'bold'}}>{t('job_lifecycle.content.previous_status_accumulated_time')}:</span> {lifecycleData.durations.humanReadableTotal}
</li>
}
{lifecycleData.lifecycle[0] && lifecycleData.lifecycle[0].value && lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable &&
<li>
<span
style={{fontWeight: 'bold'}}>{t('job_lifecycle.content.current_status_accumulated_time')} ({lifecycleData.lifecycle[0].value}):</span> {lifecycleData.durations.totalCurrentStatusDuration.humanReadable}
</li>
}
</ul>
</Card>
: null}
</Card>
<Card type='inner' title={(
<>
<Space direction="horizontal" size="small">
<Badge status='processing' count={lifecycleData.lifecycle.length}/>
{t('job_lifecycle.content.title_transitions')}
</Space>
</>
)}>
<Table style={{
overflow: 'auto',
width: '100%',
}} columns={columns} dataSource={lifecycleData.lifecycle}/>
</Card>
</Space>
) : (
<Card type='inner' style={{textAlign: 'center'}}>
{t('job_lifecycle.content.data_unavailable')}
</Card>
)
) : (
<Card type='inner' title={t('job_lifecycle.content.title_loading')}>
{t('job_lifecycle.content.loading')}
</Card>
)}
</Card>
);
}
export default JobLifecycleComponent;

View File

@@ -1,16 +1,16 @@
import { useMutation, useLazyQuery } from "@apollo/client";
import { CheckCircleOutlined } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import {
Button,
Card,
Form,
InputNumber,
notification,
Popover,
Space,
notification,
} from "antd";
import moment from "moment";
import React, { useState, useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import {
@@ -50,6 +50,7 @@ export default function ScoreboardAddButton({
const handleFinish = async (values) => {
logImEXEvent("job_close_add_to_scoreboard");
values.date = moment(values.date).format("YYYY-MM-DD");
setLoading(true);
let result;
@@ -177,7 +178,7 @@ export default function ScoreboardAddButton({
return acc + job.lbr_adjustments[val];
}, 0);
form.setFieldsValue({
date: new moment(),
date: moment(),
bodyhrs: Math.round(v.bodyhrs * 10) / 10,
painthrs: Math.round(v.painthrs * 10) / 10,
});

View File

@@ -53,12 +53,14 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
);
return (
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
<Button shape="round">
<span>{job.status}</span>
<>
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
<Button shape="round">
<span>{job.status}</span>
<DownCircleFilled />
</Button>
</Dropdown>
<DownCircleFilled />
</Button>
</Dropdown>
</>
);
}

View File

@@ -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 (
<>
<Button loading={loading} onClick={handleDelete}>
{t("jobs.labels.deleteintake")}
</Button>
<Button loading={loading} onClick={handleDeleteDelivery}>
{t("jobs.labels.deletedelivery")}
</Button>
<Space wrap>
<Button
loading={loading}
onClick={handleDelete}
disabled={!job.intakechecklist}
>
{t("jobs.labels.deleteintake")}
</Button>
<Button
loading={loading}
onClick={handleDeleteDelivery}
disabled={!job.deliverychecklist}
>
{t("jobs.labels.deletedelivery")}
</Button>
</Space>
</>
);
}

View File

@@ -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 (
<>
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleMarkForExport}
>
{t("jobs.labels.markforreexport")}
</Button>
<Button
loading={loading}
disabled={job.date_exported}
onClick={handleMarkExported}
>
{t("jobs.actions.markasexported")}
</Button>
<Button
loading={loading}
disabled={!job.date_invoiced || job.date_exported}
onClick={handleUninvoice}
>
{t("jobs.actions.uninvoice")}
</Button>
<Space wrap>
<Button
loading={loading}
disabled={!job.date_exported}
onClick={handleMarkForExport}
>
{t("jobs.labels.markforreexport")}
</Button>
<Button
loading={loading}
disabled={job.date_exported}
onClick={handleMarkExported}
>
{t("jobs.actions.markasexported")}
</Button>
<Button
loading={loading}
disabled={!job.date_invoiced || job.date_exported}
onClick={handleUninvoice}
>
{t("jobs.actions.uninvoice")}
</Button>
</Space>
</>
);
}

View File

@@ -0,0 +1,65 @@
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";
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 [mutationUpdateRemoveFromAR] = useMutation(UPDATE_REMOVE_FROM_AR);
const handleChange = async (value) => {
setLoading(true);
const result = await mutationUpdateRemoveFromAR({
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 (
<>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginRight: "10px" }}>
{t("jobs.labels.remove_from_ar")}:
</div>
<div>
<Switch
checked={switchValue}
loading={loading}
onChange={handleChange}
/>
</div>
</div>
</>
);
}

View File

@@ -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 (
<Button loading={loading} disabled={!job.voided} onClick={handleUpdate}>
{t("jobs.actions.unvoid")}
</Button>
<>
<Button loading={loading} disabled={!job.voided} onClick={handleUpdate}>
{t("jobs.actions.unvoid")}
</Button>
</>
);
}

View File

@@ -6,10 +6,10 @@ import {
useQuery,
} from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import { Col, notification, Row } from "antd";
import { Col, Row, notification } from "antd";
import Axios from "axios";
import Dinero from "dinero.js";
import moment from "moment";
import moment from "moment-business-days";
import queryString from "query-string";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -30,8 +30,8 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import confirmDialog from "../../utils/asyncConfirm";
import CriticalPartsScan from "../../utils/criticalPartsScan";
import AlertComponent from "../alert/alert.component";
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
@@ -73,7 +73,15 @@ export function JobsAvailableContainer({
const [selectedJob, setSelectedJob] = useState(null);
const [selectedOwner, setSelectedOwner] = useState(null);
const [partsQueueToggle, setPartsQueueToggle] = useState(bodyshop.md_functionality_toggles.parts_queue_toggle);
const [partsQueueToggle, setPartsQueueToggle] = useState(
bodyshop.md_functionality_toggles.parts_queue_toggle
);
const [updateSchComp, setSchComp] = useState({
actual_in: moment(),
checked: false,
scheduled_completion: moment(),
automatic: false,
});
const [insertLoading, setInsertLoading] = useState(false);
@@ -197,11 +205,16 @@ export function JobsAvailableContainer({
notification["error"]({
message: t("jobs.errors.creating", { error: err.message }),
});
refetch().catch(e => {console.error(`Something went wrong in jobs available table container - ${err.message || ''}`)});
refetch().catch((e) => {
console.error(
`Something went wrong in jobs available table container - ${
err.message || ""
}`
);
});
setInsertLoading(false);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
}
};
//Supplement scenario
@@ -225,6 +238,22 @@ export function JobsAvailableContainer({
//IO-539 Check for Parts Rate on PAL for SGI use case.
await CheckTaxRates(supp, bodyshop);
if (updateSchComp.checked === true) {
if (updateSchComp.automatic === true) {
const job_hrs = supp.joblines.data.reduce(
(acc, val) => acc + val.mod_lb_hrs,
0
);
const num_days = job_hrs / bodyshop.target_touchtime;
supp.actual_in = updateSchComp.actual_in;
supp.scheduled_completion = moment(
updateSchComp.actual_in
).businessAdd(num_days, "days");
} else {
supp.scheduled_completion = updateSchComp.scheduled_completion;
}
}
delete supp.owner;
delete supp.vehicle;
delete supp.ins_co_nm;
@@ -261,9 +290,9 @@ export function JobsAvailableContainer({
},
});
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
if (CriticalPartsScanning.treatment === "on") {
if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id);
}
if (updateResult.errors) {
@@ -367,7 +396,6 @@ export function JobsAvailableContainer({
if (error) return <AlertComponent type="error" message={error.message} />;
return (
<LoadingSpinner
loading={insertLoading}
@@ -384,7 +412,6 @@ export function JobsAvailableContainer({
visible={ownerModalVisible}
onOk={onOwnerFindModalOk}
onCancel={onOwnerModalCancel}
/>
<JobsFindModalContainer
loading={estDataRaw.loading}
@@ -398,6 +425,8 @@ export function JobsAvailableContainer({
modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
updateSchComp={updateSchComp}
setSchComp={setSchComp}
/>
<Row gutter={[16, 16]}>
<Col span={24}>

View File

@@ -131,12 +131,10 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
))}
</DataLabel>
)}
<DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} />
</DataLabel>
<Space>
<Space wrap>
{job.special_coverage_policy && (
<Tag color="tomato">
<Space>

View File

@@ -1,9 +1,11 @@
import { SyncOutlined } from "@ant-design/icons";
import { Checkbox, Divider, Input, Table, Button } from "antd";
import React from "react";
import { Button, Checkbox, Divider, Input, Space, Table } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import PhoneFormatter from "../../utils/PhoneFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function JobsFindModalComponent({
@@ -16,11 +18,13 @@ export default function JobsFindModalComponent({
jobsListRefetch,
partsQueueToggle,
setPartsQueueToggle,
updateSchComp,
setSchComp,
}) {
const { t } = useTranslation();
const [modalSearch, setModalSearch] = modalSearchState;
const [importOptions, setImportOptions] = importOptionsState;
const [checkUTT, setCheckUTT] = useState(false);
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -142,6 +146,35 @@ export default function JobsFindModalComponent({
if (record) {
if (record.id) {
setSelectedJob(record.id);
if (record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: record.scheduled_completion,
});
} else {
if (record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: moment(),
});
}
if (!record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(record.scheduled_completion),
});
}
if (!record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(),
});
}
}
return;
}
}
@@ -177,6 +210,35 @@ export default function JobsFindModalComponent({
rowSelection={{
onSelect: (props) => {
setSelectedJob(props.id);
if (props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: props.scheduled_completion,
});
} else {
if (props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: moment(),
});
}
if (!props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(props.scheduled_completion),
});
}
if (!props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(),
});
}
}
},
type: "radio",
selectedRowKeys: [selectedJob],
@@ -190,23 +252,58 @@ export default function JobsFindModalComponent({
}}
/>
<Divider />
<Checkbox
defaultChecked={importOptions.overrideHeader}
onChange={(e) =>
setImportOptions({
...importOptions,
overrideHeaders: e.target.checked,
})
}
>
{t("jobs.labels.override_header")}
</Checkbox>
<Checkbox
<Space>
<Checkbox
defaultChecked={importOptions.overrideHeader}
onChange={(e) =>
setImportOptions({
...importOptions,
overrideHeaders: e.target.checked,
})
}
>
{t("jobs.labels.override_header")}
</Checkbox>
<Checkbox
checked={partsQueueToggle}
onChange={(e) => setPartsQueueToggle(e.target.checked)}
>
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
</Checkbox>
>
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
</Checkbox>
<Checkbox
checked={updateSchComp.checked}
onChange={(e) =>
setSchComp({ ...updateSchComp, checked: e.target.checked })
}
>
{t("jobs.labels.update_scheduled_completion")}
</Checkbox>
{updateSchComp.checked === true ? (
<>
{checkUTT === false ? (
<FormDateTimePickerComponent
value={updateSchComp.scheduled_completion}
onChange={(e) => {
setSchComp({ ...updateSchComp, scheduled_completion: e });
}}
/>
) : null}
<Checkbox
checked={checkUTT}
onChange={(e) => {
setCheckUTT(e.target.checked);
setSchComp({
...updateSchComp,
scheduled_completion: null,
automatic: true,
});
}}
>
{t("jobs.labels.calc_scheuled_completion")}
</Checkbox>
</>
) : null}
</Space>
</div>
);
}

View File

@@ -26,6 +26,8 @@ export default connect(
modalSearchState,
partsQueueToggle,
setPartsQueueToggle,
updateSchComp,
setSchComp,
...modalProps
}) {
const { t } = useTranslation();
@@ -95,6 +97,8 @@ export default connect(
modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle}
updateSchComp={updateSchComp}
setSchComp={setSchComp}
/>
) : null}
</Modal>

View File

@@ -1,8 +1,8 @@
import {
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
SyncOutlined,
BranchesOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
SyncOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";

View File

@@ -0,0 +1,77 @@
import { useQuery } from "@apollo/client";
import { Card, Divider, Drawer, Grid } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { QUERY_PARTS_QUEUE_CARD_DETAILS } from "../../graphql/jobs.queries";
import AlertComponent from "../alert/alert.component";
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsQueueJobLinesComponent from "./parts-queue-job-lines.component";
export default function PartsQueueDetailCard() {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "60%",
};
const drawerPercentage = selectedBreakpoint
? bpoints[selectedBreakpoint[0]]
: "100%";
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const history = useHistory();
const { loading, error, data } = useQuery(QUERY_PARTS_QUEUE_CARD_DETAILS, {
variables: { id: selected },
skip: !selected,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { t } = useTranslation();
const handleDrawerClose = () => {
delete searchParams.selected;
history.push({
search: queryString.stringify({
...searchParams,
}),
});
};
return (
<Drawer
visible={!!selected}
destroyOnClose
width={drawerPercentage}
placement="right"
onClose={handleDrawerClose}
>
{loading ? <LoadingSpinner /> : null}
{error ? <AlertComponent message={error.message} type="error" /> : null}
{data ? (
<Card
title={
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
{data.jobs_by_pk.ro_number || t("general.labels.na")}
</Link>
}
>
<JobsDetailHeader job={data ? data.jobs_by_pk : null} />
<Divider type="horizontal" />
<PartsQueueJobLinesComponent
jobLines={data.jobs_by_pk ? data.jobs_by_pk.joblines : null}
/>
</Card>
) : null}
</Drawer>
);
}

View File

@@ -0,0 +1,208 @@
import { Card, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort } from "../../utils/sorters";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({});
export function PartsQueueJobLinesComponent({ jobRO, loading, jobLines }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {},
});
const { t } = useTranslation();
const columns = [
{
title: "#",
dataIndex: "line_no",
key: "line_no",
sorter: (a, b) => a.line_no - b.line_no,
sortOrder:
state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
},
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
onCell: (record) => ({
className: record.manual_line && "job-line-manual",
style: {
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
},
}),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true,
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) =>
`${record.oem_partno || ""} ${
record.alt_partno ? `(${record.alt_partno})` : ""
}`.trim(),
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
filteredValue: state.filteredInfo.part_type || null,
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
sortOrder:
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
filters: [
{
text: t("jobs.labels.partsfilter"),
value: [
"PAN",
"PAC",
"PAR",
"PAL",
"PAA",
"PAM",
"PAP",
"PAS",
"PASL",
"PAG",
],
},
{
text: t("joblines.fields.part_types.PAN"),
value: ["PAN"],
},
{
text: t("joblines.fields.part_types.PAP"),
value: ["PAP"],
},
{
text: t("joblines.fields.part_types.PAL"),
value: ["PAL"],
},
{
text: t("joblines.fields.part_types.PAA"),
value: ["PAA"],
},
{
text: t("joblines.fields.part_types.PAG"),
value: ["PAG"],
},
{
text: t("joblines.fields.part_types.PAS"),
value: ["PAS"],
},
{
text: t("joblines.fields.part_types.PASL"),
value: ["PASL"],
},
{
text: t("joblines.fields.part_types.PAC"),
value: ["PAC"],
},
{
text: t("joblines.fields.part_types.PAR"),
value: ["PAR"],
},
{
text: t("joblines.fields.part_types.PAM"),
value: ["PAM"],
},
],
onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) =>
record.part_type
? t(`joblines.fields.part_types.${record.part_type}`)
: null,
},
{
title: t("joblines.fields.part_qty"),
dataIndex: "part_qty",
key: "part_qty",
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => (
<CurrencyFormatter>
{record.db_ref === "900510" || record.db_ref === "900511"
? record.prt_dsmk_m
: record.act_price}
</CurrencyFormatter>
),
},
{
title: t("joblines.fields.location"),
dataIndex: "location",
key: "location",
},
{
title: t("joblines.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: state.filteredInfo.status || null,
filters:
(jobLines &&
jobLines
.map((l) => l.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState((state) => ({
...state,
filteredInfo: filters,
sortedInfo: sorter,
}));
};
return (
<Card title={t("jobs.labels.parts_lines")}>
<Table
columns={columns}
rowKey="id"
loading={loading}
pagination={false}
dataSource={jobLines}
onChange={handleTableChange}
/>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(PartsQueueJobLinesComponent);

View File

@@ -8,30 +8,28 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component";
import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import {pageLimit} from "../../utils/config";
import AlertComponent from "../alert/alert.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function PartsQueuePageComponent({ bodyshop }) {
export function PartsQueueListComponent({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const {
//page,
sortcolumn,
sortorder,
statusFilters,
} = searchParams;
const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
const history = useHistory();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
@@ -39,19 +37,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
// offset: page ? (page - 1) * 25 : 0,
// limit: 25,
statuses: (statusFilters && JSON.parse(statusFilters)) ||
bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
order: [
{
[sortcolumn || "ro_number"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
],
},
});
@@ -107,6 +94,19 @@ export function PartsQueuePageComponent({ bodyshop }) {
history.push({ search: queryString.stringify(searchParams) });
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history.push({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
}
};
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -125,7 +125,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
title: t("jobs.fields.owner"),
dataIndex: "ownr_ln",
key: "ownr_ln",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => {
return record.ownerid ? (
@@ -139,6 +140,56 @@ export function PartsQueuePageComponent({ bodyshop }) {
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder: sortcolumn === "vehicle" && sortorder,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("jobs.fields.ins_co_nm_short"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder: sortcolumn === "ins_co_nm" && sortorder,
filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
.map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
@@ -170,23 +221,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder: sortcolumn === "scheduled_completion" && sortorder,
render: (text, record) => (
<DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter>
),
},
// {
// title: t("vehicles.fields.plate_no"),
@@ -198,14 +242,6 @@ export function PartsQueuePageComponent({ bodyshop }) {
// return record.plate_no ? record.plate_no : "";
// },
// },
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: sortcolumn === "clm_no" && sortorder,
},
// {
// title: t("jobs.fields.clm_total"),
// dataIndex: "clm_total",
@@ -307,9 +343,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
style={{ height: "100%" }}
scroll={{ x: true }}
onChange={handleTableChange}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
/>
</Card>
);
}
export default connect(mapStateToProps, null)(PartsQueuePageComponent);
export default connect(mapStateToProps, null)(PartsQueueListComponent);

View File

@@ -115,7 +115,7 @@ export function ProductionBoardKanbanComponent({
// console.log("==> New Card is somewhere in the middle");
movedCardNewKanbanParent = newChildCard.kanbanparent;
} else {
throw new Error("==> !!!!!!Couldn't find a parent.!!!! <==");
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
}
const newChildCardNewParent = newChildCard ? card.id : null;
const update = await client.mutate({
@@ -153,7 +153,7 @@ export function ProductionBoardKanbanComponent({
0
)
.toFixed(1);
const totalLAB = data
const totalLAB = data
.reduce(
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0),
0

View File

@@ -0,0 +1,273 @@
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd";
import React, {useEffect, useState} from "react";
import {fetchFilterData} from "../../utils/RenderTemplate";
import {DeleteFilled} from "@ant-design/icons";
import {useTranslation} from "react-i18next";
import {getOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
export default function ReportCenterModalFiltersSortersComponent({form}) {
return (
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
return <RenderFilters form={form} templateId={key}/>;
}}
</Form.Item>
);
}
function RenderFilters({templateId, form}) {
const [state, setState] = useState(null);
const [visible, setVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation();
useEffect(() => {
const fetch = async () => {
setIsLoading(true);
const data = await fetchFilterData({name: templateId});
if (data?.success) {
setState(data.data);
} else {
setState(null);
}
setIsLoading(false);
};
if (templateId) {
fetch();
}
}, [templateId]);
// Conditional display of filters and sorters
if (!templateId) return null;
if (isLoading) return <LoadingSkeleton/>;
if (!state) return null;
// Filters and Sorters data available
return (
<div style={{marginTop: '10px'}}>
<Checkbox
checked={visible}
onChange={(e) => setVisible(e.target.checked)}
children={t('reportcenter.labels.advanced_filters')}
/>
{visible && (
<div>
{state.filters && state.filters.length > 0 && (
<Card type='inner' title={ t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
<Form.List name={["filters"]}>
{(fields, {add, remove, move}) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={10}>
<Form.Item
key={`${index}field`}
label="field"
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={
state.filters
? state.filters.map((f) => {
return {
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}
})
: []
}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item dependencies={[['filters', field.name, "field"]]}>
{
() => {
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = state.filters.find(f => f.name === name)?.type;
return <Form.Item
key={`${index}operator`}
label="operator"
name={[field.name, "operator"]}
dependencies={[]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select options={getOperatorsByType(type)}/>
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={6}>
<Form.Item dependencies={[['filters', field.name, "field"]]}>
{
() => {
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = state.filters.find(f => f.name === name)?.type;
return <Form.Item
key={`${index}value`}
label="value"
name={[field.name, "value"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
{type === 'number' ?
<InputNumber
onChange={(value) => {
form.setFieldsValue({[field.name]: {value: parseInt(value)}});
}}
/>
:
<Input
onChange={(value) => {
form.setFieldsValue({[field.name]: {value: value.toString()}});
}}
/>
}
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem"}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
)}
{state.sorters && state.sorters.length > 0 && (
<Card type='inner' title={ t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
<Form.List name={["sorters"]}>
{(fields, {add, remove, move}) => {
return (
<div>
Sorters
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={11}>
<Form.Item
key={`${index}field`}
label="field"
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={
state.sorters
? state.sorters.map((f) => ({
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}))
: []
}
/>
</Form.Item>
</Col>
<Col span={11}>
<Form.Item
key={`${index}direction`}
label="direction"
name={[field.name, "direction"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={[
{value: "desc", label: "Descending"},
{value: "asc", label: "Ascending"},
]}
/>
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem"}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
)}
</div>
)}
</div>
);
}

View File

@@ -1,30 +1,22 @@
import { useLazyQuery } from "@apollo/client";
import {
Button,
Card,
Col,
DatePicker,
Form,
Input,
Radio,
Row,
Typography,
} from "antd";
import {useLazyQuery} from "@apollo/client";
import {Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography,} from "antd";
import _ from "lodash";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { selectReportCenter } from "../../redux/modals/modals.selectors";
import DatePIckerRanges from "../../utils/DatePickerRanges";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {QUERY_ACTIVE_EMPLOYEES} from "../../graphql/employees.queries";
import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries";
import {selectReportCenter} from "../../redux/modals/modals.selectors";
import DatePickerRanges from "../../utils/DatePickerRanges";
import {GenerateDocument} from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants";
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import "./report-center-modal.styles.scss";
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
const mapStateToProps = createStructuredSelector({
reportCenterModal: selectReportCenter,
});
@@ -32,274 +24,286 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(ReportCenterModalComponent);
export function ReportCenterModalComponent({ reportCenterModal }) {
export function ReportCenterModalComponent({reportCenterModal}) {
const [form] = Form.useForm();
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const {t} = useTranslation();
const Templates = TemplateList("report_center");
const ReportsList = Object.keys(Templates).map((key) => {
return Templates[key];
});
const { visible } = reportCenterModal;
const {open} = reportCenterModal;
const [callVendorQuery, { data: vendorData, called: vendorCalled }] =
useLazyQuery(QUERY_ALL_VENDORS, {
skip: !(
visible &&
Templates[form.getFieldValue("key")] &&
Templates[form.getFieldValue("key")].idtype
),
});
const [callVendorQuery, {data: vendorData, called: vendorCalled}] =
useLazyQuery(QUERY_ALL_VENDORS, {
skip: !(
open &&
Templates[form.getFieldValue("key")] &&
Templates[form.getFieldValue("key")].idtype
),
});
const [callEmployeeQuery, { data: employeeData, called: employeeCalled }] =
useLazyQuery(QUERY_ACTIVE_EMPLOYEES, {
skip: !(
visible &&
Templates[form.getFieldValue("key")] &&
Templates[form.getFieldValue("key")].idtype
),
});
const [callEmployeeQuery, {data: employeeData, called: employeeCalled}] =
useLazyQuery(QUERY_ACTIVE_EMPLOYEES, {
skip: !(
open &&
Templates[form.getFieldValue("key")] &&
Templates[form.getFieldValue("key")].idtype
),
});
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(
{
name: values.key,
variables: {
...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
: {}),
...(end
? { end: moment(end).endOf("day").format("YYYY-MM-DD") }
: {}),
...(start ? { starttz: moment(start).startOf("day") } : {}),
...(end ? { endtz: moment(end).endOf("day") } : {}),
{
name: values.key,
variables: {
...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
: {}),
...(end ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } : {}),
...(start ? { starttz: moment(start).startOf("day") } : {}),
...(end ? { endtz: moment(end).endOf("day") } : {}),
...(id ? { id: id } : {}),
...(id ? { id: id } : {}),
},
filters: values.filters,
sorters: values.sorters,
},
},
{
to: values.to,
subject: Templates[values.key]?.subject,
},
values.sendbyexcel === "excel"
? "x"
: values.sendby === "email"
? "e"
: "p",
id
{
to: values.to,
subject: Templates[values.key]?.subject,
},
values.sendbyexcel === "excel"
? "x"
: values.sendby === "email"
? "e"
: "p",
id
);
setLoading(false);
};
const FilteredReportsList =
search !== ""
? ReportsList.filter((r) =>
r.title.toLowerCase().includes(search.toLowerCase())
)
: ReportsList;
search !== ""
? ReportsList.filter((r) =>
r.title.toLowerCase().includes(search.toLowerCase())
)
: ReportsList;
//Group it, create cards, and then filter out.
const grouped = _.groupBy(FilteredReportsList, "group");
return (
<div>
<Form
onFinish={handleFinish}
autoComplete={"off"}
layout="vertical"
form={form}
>
<Input.Search
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
<Form.Item
name="key"
label={t("reportcenter.labels.key")}
// className="radio-group-columns"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
<div>
<Form
onFinish={handleFinish}
autoComplete={"off"}
layout="vertical"
form={form}
>
<Radio.Group>
{/* {Object.keys(Templates).map((key) => (
<Input.Search
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
<Form.Item
name="key"
label={t("reportcenter.labels.key")}
// className="radio-group-columns"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Radio.Group>
{/* {Object.keys(Templates).map((key) => (
<Radio key={key} value={key}>
{Templates[key].title}
</Radio>
))} */}
<Row gutter={[16, 16]}>
{Object.keys(grouped).map((key) => (
<Col md={8} sm={12} key={key}>
<Card.Grid
style={{
width: "100%",
height: "100%",
maxHeight: "33vh",
overflowY: "scroll",
}}
>
<Typography.Title level={4}>
{t(`reportcenter.labels.groups.${key}`)}
</Typography.Title>
<ul style={{ columns: "2 auto" }}>
{grouped[key].map((item) => (
<li key={item.key}>
<Radio key={item.key} value={item.key}>
{item.title}
</Radio>
</li>
))}
</ul>
</Card.Grid>
</Col>
))}
</Row>
</Radio.Group>
</Form.Item>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
if (!key) return null;
//Kind of Id
const rangeFilter = Templates[key] && Templates[key].rangeFilter;
if (!rangeFilter) return null;
return (
<div>
{t("reportcenter.labels.filterson", {
object: rangeFilter.object,
field: rangeFilter.field,
})}
</div>
);
}}
</Form.Item>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
const currentId = form.getFieldValue("id");
if (!key) return null;
//Kind of Id
const idtype = Templates[key] && Templates[key].idtype;
if (!idtype && currentId) {
form.setFieldsValue({ id: null });
return null;
}
if (!vendorCalled && idtype === "vendor") callVendorQuery();
if (!employeeCalled && idtype === "employee") callEmployeeQuery();
if (idtype === "vendor")
<Row gutter={[16, 16]}>
{Object.keys(grouped).map((key) => (
<Col md={8} sm={12} key={key}>
<Card.Grid
style={{
width: "100%",
height: "100%",
maxHeight: "33vh",
overflowY: "scroll",
}}
>
<Typography.Title level={4}>
{t(`reportcenter.labels.groups.${key}`)}
</Typography.Title>
<ul style={{ listStyleType: 'none', columns: "2 auto"}}>
{grouped[key].map((item) => (
<li key={item.key}>
<Radio key={item.key} value={item.key}>
{item.title}
</Radio>
</li>
))}
</ul>
</Card.Grid>
</Col>
))}
</Row>
</Radio.Group>
</Form.Item>
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
if (!key) return null;
//Kind of Id
const rangeFilter = Templates[key] && Templates[key].rangeFilter;
if (!rangeFilter) return null;
return (
<Form.Item
name="id"
label={t("reportcenter.labels.vendor")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<VendorSearchSelect
options={vendorData ? vendorData.vendors : []}
/>
</Form.Item>
<div>
{t("reportcenter.labels.filterson", {
object: rangeFilter.object,
field: rangeFilter.field,
})}
</div>
);
if (idtype === "employee")
return (
<Form.Item
name="id"
label={t("reportcenter.labels.employee")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<EmployeeSearchSelect
options={employeeData ? employeeData.employees : []}
/>
</Form.Item>
);
else return null;
}}
</Form.Item>
<Form.Item
name="dates"
label={t("reportcenter.labels.dates")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
ranges={DatePIckerRanges}
/>
</Form.Item>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
//Kind of Id
const reporttype = Templates[key] && Templates[key].reporttype;
}}
</Form.Item>
<ReportCenterModalFiltersSortersComponent form={form} />
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
const currentId = form.getFieldValue("id");
if (!key) return null;
//Kind of Id
const idtype = Templates[key] && Templates[key].idtype;
if (!idtype && currentId) {
form.setFieldsValue({id: null});
return null;
}
if (!vendorCalled && idtype === "vendor") callVendorQuery();
if (!employeeCalled && idtype === "employee") callEmployeeQuery();
if (idtype === "vendor")
return (
<Form.Item
name="id"
label={t("reportcenter.labels.vendor")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<VendorSearchSelect
options={vendorData ? vendorData.vendors : []}
/>
</Form.Item>
);
if (idtype === "employee")
return (
<Form.Item
name="id"
label={t("reportcenter.labels.employee")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<EmployeeSearchSelect
options={employeeData ? employeeData.employees : []}
/>
</Form.Item>
);
else return null;
}}
</Form.Item>
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
const datedisable = Templates[key] && Templates[key].datedisable;
if (datedisable !== true) {
return (
<Form.Item
name="dates"
label={t("reportcenter.labels.dates")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
presets={DatePickerRanges}
/>
</Form.Item>
);
} else return null;
}}
</Form.Item>
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
//Kind of Id
const reporttype = Templates[key] && Templates[key].reporttype;
if (reporttype === "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendbyexcel"
initialValue="excel"
>
<Radio.Group>
<Radio value="excel">{t("general.labels.excel")}</Radio>
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
if (reporttype === "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendbyexcel"
initialValue="excel"
>
<Radio.Group>
<Radio value="excel">{t("general.labels.excel")}</Radio>
</Radio.Group>
</Form.Item>
);
if (reporttype !== "excel")
return (
<Form.Item
label={t("general.labels.sendby")}
name="sendby"
initialValue="print"
>
<Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "1rem",
}}
>
<Button onClick={() => form.submit()} style={{}} loading={loading}>
{t("reportcenter.actions.generate")}
</Button>
</div>
</Form>
</div>
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "1rem",
}}
>
<Button onClick={() => form.submit()} style={{}} loading={loading}>
{t("reportcenter.actions.generate")}
</Button>
</div>
</Form>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Form } from "antd";
import React from "react";
import ConfigFormComponents from "../config-form-components/config-form-components.component";
export default function ShopCsiConfigForm({ selectedCsi }) {
@@ -9,7 +9,7 @@ export default function ShopCsiConfigForm({ selectedCsi }) {
return (
<div>
The Config Form {readOnly}
{readOnly}
{selectedCsi && (
<Form form={form} onFinish={handleFinish}>
<ConfigFormComponents

View File

@@ -1,7 +1,7 @@
import { CheckCircleFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Col, List, Row } from "antd";
import React, { useState } from "react";
import { useQuery } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { GET_ALL_QUESTION_SETS } from "../../graphql/csi.queries";
import { DateFormatter } from "../../utils/DateFormatter";
@@ -21,7 +21,6 @@ export default function ShopCsiConfig() {
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
The Config Form
<Row>
<Col span={3}>
<List
@@ -42,7 +41,8 @@ export default function ShopCsiConfig() {
)}
/>
</Col>
<Col span={21}>
<Col span={1} />
<Col span={20}>
<ShopCsiConfigForm selectedCsi={selectedCsi} />
</Col>
</Row>

View File

@@ -1,17 +1,21 @@
import { onError } from "@apollo/client/link/error";
//https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth
import * as Sentry from "@sentry/react";
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
);
Sentry.captureException({ message, locations, path });
});
}
if (networkError)
console.log(`[Network error]: ${JSON.stringify(networkError)}`);
console.log(operation.getContext());
return forward(operation);
}
);

View File

@@ -22,6 +22,7 @@ export const QUERY_AVAILABLE_CC = gql`
]
status: { _eq: "courtesycars.status.in" }
}
order_by: { fleetnumber: asc }
) {
color
dailycost
@@ -57,7 +58,7 @@ export const CHECK_CC_FLEET_NUMBER = gql`
`;
export const QUERY_ALL_CC = gql`
query QUERY_ALL_CC {
courtesycars {
courtesycars(order_by: { fleetnumber: asc }) {
color
created_at
dailycost

View File

@@ -57,19 +57,15 @@ export const INSERT_CSI = gql`
`;
export const QUERY_CSI_RESPONSE_PAGINATED = gql`
query QUERY_CSI_RESPONSE_PAGINATED(
$offset: Int
$limit: Int
$order: [csi_order_by!]!
) {
csi(offset: $offset, limit: $limit, order_by: $order) {
query QUERY_CSI_RESPONSE_PAGINATED {
csi(order_by: { completedon: desc_nulls_last }) {
id
completedon
job {
ownr_fn
ownr_ln
ownerid
ro_number
id
}
}
@@ -83,6 +79,7 @@ export const QUERY_CSI_RESPONSE_PAGINATED = gql`
export const QUERY_CSI_RESPONSE_BY_PK = gql`
query QUERY_CSI_RESPONSE_BY_PK($id: uuid!) {
csi_by_pk(id: $id) {
completedon
relateddata
valid
validuntil

View File

@@ -108,12 +108,7 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
`;
export const QUERY_PARTS_QUEUE = gql`
query QUERY_PARTS_QUEUE(
$statuses: [String!]!
$offset: Int
$limit: Int
$order: [jobs_order_by!]
) {
query QUERY_PARTS_QUEUE($statuses: [String!]!, $offset: Int, $limit: Int) {
jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) {
aggregate {
count(distinct: true)
@@ -125,7 +120,7 @@ export const QUERY_PARTS_QUEUE = gql`
}
offset: $offset
limit: $limit
order_by: $order
order_by: { ro_number: desc }
) {
ownr_fn
ownr_ln
@@ -142,7 +137,9 @@ export const QUERY_PARTS_QUEUE = gql`
v_color
vehicleid
scheduled_in
scheduled_completion
id
ins_co_nm
clm_no
ro_number
status
@@ -545,147 +542,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 +718,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 +775,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 +897,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) {
@@ -2225,3 +2218,280 @@ 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
}
}
`;
export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
query QUERY_JOB_CARD_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
actual_completion
actual_delivery
actual_in
alt_transport
available_jobs {
id
}
area_of_damage
ca_gst_registrant
cccontracts {
agreementnumber
courtesycar {
id
make
model
year
plate
fleetnumber
}
id
scheduledreturn
start
status
}
clm_no
clm_total
comment
date_estimated
date_exported
date_invoiced
date_last_contacted
date_next_contact
date_open
date_repairstarted
date_scheduled
ded_amt
employee_body
employee_body_rel {
id
first_name
last_name
}
employee_csr
employee_csr_rel {
id
first_name
last_name
}
employee_prep
employee_prep_rel {
id
first_name
last_name
}
employee_refinish
employee_refinish_rel {
id
first_name
last_name
}
est_co_nm
est_ct_fn
est_ct_ln
est_ea
est_ph1
id
ins_co_nm
ins_ct_fn
ins_ct_ln
ins_ea
ins_ph1
inproduction
job_totals
joblines(
order_by: { line_no: asc }
where: {
part_type: {
_in: [
"PAN"
"PAC"
"PAR"
"PAL"
"PAA"
"PAM"
"PAP"
"PAG"
]
}
removed: { _eq: false }
}
) {
act_price
alt_partno
db_ref
id
line_desc
line_no
location
mod_lbr_ty
mod_lb_hrs
oem_partno
part_qty
part_type
prt_dsmk_m
status
}
lbr_adjustments
ownr_co_nm
ownr_ea
ownr_fn
ownr_ln
ownr_ph1
ownr_ph2
owner {
id
allow_text_message
preferred_contact
tax_number
}
owner_owing
plate_no
plate_st
po_number
production_vars
ro_number
scheduled_completion
scheduled_delivery
scheduled_in
special_coverage_policy
status
suspended
updated_at
vehicle {
id
jobs {
id
clm_no
ro_number
}
notes
plate_no
v_color
v_make_desc
v_model_desc
v_model_yr
}
vehicleid
v_color
v_make_desc
v_model_desc
v_model_yr
v_vin
voided
}
}
`;

View File

@@ -14,38 +14,38 @@ import { persistor, store } from "./redux/store";
import reportWebVitals from "./reportWebVitals";
import "./translations/i18n";
import "./utils/CleanAxios";
//import { BrowserTracing } from "@sentry/tracing";
// Dinero.defaultCurrency = "CAD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
if (process.env.NODE_ENV !== "development") {
Sentry.init({
dsn: "https://fd7e89369b6b4bdc9c6c4c9f22fa4ee4@o492140.ingest.sentry.io/5651027",
ignoreErrors: [
"ResizeObserver loop",
"Module specifier, 'fs' does not start",
"Module specifier, 'zlib' does not start with",
],
integrations: [
// new BrowserTracing(),
// new Sentry.Integrations.Breadcrumbs({ console: true }),
// new Sentry.Replay(),
],
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
// replaysSessionSampleRate: 0.1,
// // If the entire session is not sampled, use the below sample rate to sample
// // sessions when an error occurs.
// replaysOnErrorSampleRate: 1.0,
environment: process.env.NODE_ENV,
// tracesSampleRate: 0.2,
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
// tracesSampleRate: 0.5,
});
}
//if (process.env.NODE_ENV !== "development") {
Sentry.init({
dsn: "https://fd7e89369b6b4bdc9c6c4c9f22fa4ee4@o492140.ingest.sentry.io/5651027",
ignoreErrors: [
"ResizeObserver loop",
"Module specifier, 'fs' does not start",
"Module specifier, 'zlib' does not start with",
],
integrations: [
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: true,
}),
new Sentry.BrowserTracing({}),
],
tracePropagationTargets: [
"api.imex.online",
"api.test.imex.online",
"db.imex.online",
],
tracesSampleRate: 1.0,
replaysOnErrorSampleRate: 1.0,
environment: process.env.NODE_ENV,
});
//}
ReactDOM.render(
<Provider store={store}>

View File

@@ -1,88 +1,67 @@
import { useQuery, useMutation } from "@apollo/client";
import { Form, Layout, Typography, Button, Result } from "antd";
import React, { useState } from "react";
// import { useMutation, useQuery } from "@apollo/client";
import { Button, Form, Layout, Result, Typography } from "antd";
import axios from "axios";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useParams } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import ConfigFormComponents from "../../components/config-form-components/config-form-components.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { QUERY_SURVEY, COMPLETE_SURVEY } from "../../graphql/csi.queries";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import { DateTimeFormat } from "./../../utils/DateFormatter";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage);
export function CsiContainerPage({ currentUser }) {
const { surveyId } = useParams();
const [form] = Form.useForm();
const [axiosResponse, setAxiosResponse] = useState(null);
const [submitting, setSubmitting] = useState({
loading: false,
submitted: false,
});
const { loading, error, data } = useQuery(QUERY_SURVEY, {
variables: { surveyId },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { t } = useTranslation();
const [completeSurvey] = useMutation(COMPLETE_SURVEY);
if (loading) return <LoadingSpinner />;
if (error || !!!data.csi_by_pk)
return (
<div>
<Result
status="error"
title={t("csi.errors.notfoundtitle")}
subTitle={t("csi.errors.notfoundsubtitle")}
>
{error ? (
<div>ERROR: {error.graphQLErrors.map((e) => e.message)}</div>
) : null}
</Result>
</div>
);
const handleFinish = async (values) => {
setSubmitting({ ...submitting, loading: true });
const result = await completeSurvey({
variables: {
surveyId,
survey: {
response: values,
valid: false,
completedon: new Date(),
},
},
});
if (!!!result.errors) {
setSubmitting({ ...submitting, loading: false, submitted: true });
} else {
setSubmitting({
...submitting,
const getAxiosData = useCallback(async () => {
try {
try {
window.$crisp.push(["do", "chat:hide"]);
} catch {
console.log("Unable to attach to crisp instance. ");
}
setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true }));
const response = await axios.post("/csi/lookup", { surveyId });
setSubmitting((prevSubmitting) => ({
...prevSubmitting,
loading: false,
error: JSON.stringify(result.errors),
}));
setAxiosResponse(response.data);
} catch (error) {
console.error(`Something went wrong...: ${error.message}`);
console.dir({
stack: error?.stack,
message: error?.message,
});
}
};
}, [setAxiosResponse, surveyId]);
const {
relateddata: { bodyshop, job },
csiquestion: { config: csiquestions },
} = data.csi_by_pk;
useEffect(() => {
getAxiosData().catch((err) =>
console.error(
`Something went wrong fetching axios data: ${err.message || ""}`
)
);
}, [getAxiosData]);
if (currentUser && currentUser.authorized)
// Return if authorized
if (currentUser && currentUser.authorized) {
return (
<Layout
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
@@ -94,85 +73,176 @@ export function CsiContainerPage({ currentUser }) {
/>
</Layout>
);
}
return (
<Layout
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
<img src={bodyshop.logo_img_path.src} alt="Logo" />
) : null}
<div style={{ margin: "2em" }}>
<strong>{bodyshop.shopname || ""}</strong>
<div>{`${bodyshop.address1 || ""}`}</div>
<div>{`${bodyshop.address2 || ""}`}</div>
<div>{`${bodyshop.city || ""} ${bodyshop.state || ""} ${
bodyshop.zip_post || ""
}`}</div>
if (submitting.loading) return <LoadingSpinner />;
const handleFinish = async (values) => {
try {
setSubmitting({ ...submitting, loading: true, submitting: true });
const result = await axios.post("/csi/submit", { surveyId, values });
console.log("result", result);
if (!!!result.errors && result.data.update_csi.affected_rows > 0) {
setSubmitting({ ...submitting, loading: false, submitted: true });
}
} catch (error) {
console.error(`Something went wrong...: ${error.message}`);
console.dir({
stack: error?.stack,
message: error?.message,
});
}
};
if (!axiosResponse || axiosResponse.csi_by_pk === null) {
// Do something here , this is where you would return a loading box or something
return (
<>
<Layout style={{ display: "flex", flexDirection: "column" }}>
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
textAlign: "center",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Form>
<Result
status="error"
title={t("csi.errors.notfoundtitle")}
subTitle={t("csi.errors.notfoundsubtitle")}
/>
</Form>
</Layout.Content>
<Layout.Footer>
{t("csi.labels.copyright")}{" "}
{t("csi.fields.surveyid", { surveyId: surveyId })}
</Layout.Footer>
</Layout>
</>
);
} else {
const {
relateddata: { bodyshop, job },
csiquestion: { config: csiquestions },
} = axiosResponse.csi_by_pk;
return (
<Layout style={{ display: "flex", flexDirection: "column" }}>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
<img
src={bodyshop.logo_img_path.src}
alt={bodyshop.shopname.concat(" Logo")}
height={bodyshop.logo_img_path.height}
width={bodyshop.logo_img_path.width}
/>
) : null}
<div style={{ margin: "2em", verticalAlign: "middle" }}>
<Typography.Title level={4} style={{ margin: 0 }}>
{bodyshop.shopname || ""}
</Typography.Title>
<Typography.Paragraph style={{ margin: 0 }}>
{`${bodyshop.address1 || ""}${bodyshop.address2 ? ", " : ""}${
bodyshop.address2 || ""
}`.trim()}
</Typography.Paragraph>
<Typography.Paragraph style={{ margin: 0 }}>
{`${bodyshop.city || ""}${
bodyshop.city && bodyshop.state ? ", " : ""
}${bodyshop.state || ""} ${bodyshop.zip_post || ""}`.trim()}
</Typography.Paragraph>
</div>
</div>
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
<strong>
{t("csi.labels.greeting", {
name: job.ownr_co_nm || job.ownr_fn || "",
})}
</strong>
<Typography.Paragraph>
{t("csi.labels.intro", { shopname: bodyshop.shopname || "" })}
</Typography.Paragraph>
</div>
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
<strong>{`Hi ${job.ownr_co_nm || job.ownr_fn || ""}!`}</strong>
<Typography.Paragraph>
{`At ${
bodyshop.shopname || ""
}, we value your feedback. We would love to
hear what you have to say. Please fill out the form below.`}
</Typography.Paragraph>
</div>
{submitting.error ? (
<AlertComponent message={submitting.error} type="error" />
) : null}
{submitting.error ? (
<AlertComponent message={submitting.error} type="error" />
) : null}
{submitting.submitted ? (
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Result
status="success"
title={t("csi.successes.submitted")}
subTitle={t("csi.successes.submittedsub")}
/>
</Layout.Content>
) : (
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Form form={form} onFinish={handleFinish}>
<ConfigFormComponents componentList={csiquestions} />
<Button
loading={submitting.loading}
type="primary"
htmlType="submit"
>
{t("general.actions.submit")}
</Button>
</Form>
</Layout.Content>
)}
<Layout.Footer>
{`Copyright ImEX.Online. Survey ID: ${surveyId}`}
</Layout.Footer>
</Layout>
);
{submitting.submitted ? (
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Result
status="success"
title={t("csi.successes.submitted")}
subTitle={t("csi.successes.submittedsub")}
/>
</Layout.Content>
) : (
<Layout.Content
style={{
backgroundColor: "#fff",
margin: "2em 4em",
padding: "2em",
overflowY: "auto",
}}
>
<Form form={form} onFinish={handleFinish}>
{axiosResponse.csi_by_pk.valid ? (
<>
<ConfigFormComponents componentList={csiquestions} />
<Button
loading={submitting.loading}
type="primary"
htmlType="submit"
style={{ float: "right" }}
>
{t("general.actions.submit")}
</Button>
</>
) : (
<>
<Result
title={t("csi.errors.surveycompletetitle")}
status="warning"
subTitle={t("csi.errors.surveycompletesubtitle", {
date: DateTimeFormat(axiosResponse.csi_by_pk.completedon),
})}
/>
<Typography.Paragraph
type="secondary"
style={{ textAlign: "center" }}
>
{t("csi.successes.submittedsub")}
</Typography.Paragraph>
</>
)}
</Form>
</Layout.Content>
)}
<Layout.Footer>
{t("csi.labels.copyright")}{" "}
{t("csi.fields.surveyid", { surveyId: surveyId })}
</Layout.Footer>
</Layout>
);
}
}

View File

@@ -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 }) {
<JobsAdminMarkReexport job={data ? data.jobs_by_pk : {}} />
<JobsAdminUnvoid job={data ? data.jobs_by_pk : {}} />
<JobsAdminStatus job={data ? data.jobs_by_pk : {}} />
<JobsAdminRemoveAR job={data ? data.jobs_by_pk : {}} />
</Space>
</Card>
</Col>

View File

@@ -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,
@@ -333,7 +334,15 @@ export function JobsDetailPage({
>
<JobsDetailLaborContainer job={job} jobId={job.id} />
</Tabs.TabPane>
<Tabs.TabPane
<Tabs.TabPane
forceRender
tab={<span><BarsOutlined />{t('menus.jobsdetail.lifecycle')}</span>}
key="lifecycle"
>
<JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses}/>
</Tabs.TabPane>
<Tabs.TabPane
forceRender
tab={
<span>

View File

@@ -1,12 +1,13 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import PartsQueueDetailCard from "../../components/parts-queue-card/parts-queue-card.component";
import PartsQueueList from "../../components/parts-queue-list/parts-queue.list.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import PartsQueuePage from "./parts-queue.page.component";
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -26,7 +27,8 @@ export function PartsQueuePageContainer({ setBreadcrumbs, setSelectedHeader }) {
return (
<RbacWrapper action="jobs:partsqueue">
<PartsQueuePage />
<PartsQueueList />
<PartsQueueDetailCard />
</RbacWrapper>
);
}

View File

@@ -1,22 +1,20 @@
import { Row, Col } from "antd";
import { useQuery } from "@apollo/client";
import queryString from "query-string";
import { Col, Row } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import CsiResponseFormContainer from "../../components/csi-response-form/csi-response-form.container";
import CsiResponseListPaginated from "../../components/csi-response-list-paginated/csi-response-list-paginated.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { QUERY_CSI_RESPONSE_PAGINATED } from "../../graphql/csi.queries";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -33,28 +31,11 @@ export function ShopCsiContainer({
}) {
const { t } = useTranslation();
const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder } = searchParams;
const { loading, error, data, refetch } = useQuery(
QUERY_CSI_RESPONSE_PAGINATED,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
//search: search || "",
offset: page ? (page - 1) * pageLimit : 0,
limit: pageLimit,
order: [
{
[sortcolumn || "completedon"]: sortorder
? sortorder === "descend"
? "desc_nulls_last"
: "asc"
: "desc_nulls_last",
},
],
},
}
);
@@ -73,12 +54,7 @@ export function ShopCsiContainer({
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<RbacWrapper
action="csi:page"
// noauth={
// <AlertComponent message="You don't have acess to see this screen." />
// }
>
<RbacWrapper action="csi:page">
<Row gutter={16}>
<Col span={10}>
<CsiResponseListPaginated

View File

@@ -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.",
@@ -112,11 +113,11 @@
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
"jobinvoiced": "Job has been invoiced.",
"jobconverted": "Job converted and assigned number {{ro_number}}.",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}",
"jobinvoiced": "Job has been invoiced.",
"jobioucreated": "IOU Created.",
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
"jobnoteadded": "Note added to Job.",
@@ -254,7 +255,6 @@
"saving": "Error encountered while saving. {{message}}"
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"address1": "Address 1",
"address2": "Address 2",
"appt_alt_transport": "Appointment Alternative Transportation Options",
@@ -331,6 +331,9 @@
"md_ded_notes": "Deductible Notes",
"md_email_cc": "Auto Email CC: $t(printcenter.subjects.jobs.{{template}})",
"md_from_emails": "Additional From Emails",
"md_functionality_toggles": {
"parts_queue_toggle": "Auto Add Imported/Supplemented Jobs to Parts Queue"
},
"md_hour_split": {
"paint": "Paint Hour Split",
"prep": "Prep Hour Split"
@@ -353,9 +356,6 @@
},
"md_payment_types": "Payment Types",
"md_referral_sources": "Referral Sources",
"md_functionality_toggles": {
"parts_queue_toggle": "Auto Add Imported/Supplemented Jobs to Parts Queue"
},
"md_tasks_presets": {
"hourstype": "",
"memo": "",
@@ -473,6 +473,7 @@
"editaccess": "Users -> Edit access"
}
},
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"responsibilitycenter": "Responsibility Center",
"responsibilitycenter_accountdesc": "Account Description",
"responsibilitycenter_accountitem": "Item",
@@ -814,6 +815,10 @@
"usage": "Usage",
"vehicle": "Vehicle Description"
},
"readiness": {
"notready": "Not Ready",
"ready": "Ready"
},
"status": {
"in": "Available",
"inservice": "In Service",
@@ -823,10 +828,6 @@
},
"successes": {
"saved": "Courtesy Car saved successfully."
},
"readiness": {
"notready": "Not Ready",
"ready": "Ready"
}
},
"csi": {
@@ -837,20 +838,27 @@
"creating": "Error creating survey {{message}}",
"notconfigured": "You do not have any current CSI Question Sets configured.",
"notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.",
"notfoundtitle": "No survey found."
"notfoundtitle": "No survey found.",
"surveycompletetitle": "Survey previously completed",
"surveycompletesubtitle": "This survey was already completed on {{date}}."
},
"fields": {
"completedon": "Completed On",
"created_at": "Created At"
"created_at": "Created At",
"surveyid": "Survey ID {{surveyId}}",
"validuntil": "Valid Until"
},
"labels": {
"nologgedinuser": "Please log out of ImEX Online",
"nologgedinuser_sub": "Users of ImEX Online cannot complete CSI surveys while logged in. Please log out and try again.",
"nologgedinuser": "Please log out of $t(titles.app)",
"nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.",
"noneselected": "No response selected.",
"title": "Customer Satisfaction Survey"
"title": "Customer Satisfaction Survey",
"greeting": "Hi {{name}}!",
"intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.",
"copyright": "Copyright © $t(titles.app). All Rights Reserved."
},
"successes": {
"created": "CSI created successfully. ",
"created": "CSI created successfully.",
"submitted": "Your responses have been submitted successfully.",
"submittedsub": "Your input is highly appreciated."
}
@@ -1213,6 +1221,31 @@
"updated": "Inventory line updated."
}
},
"job_lifecycle": {
"columns": {
"duration": "Duration",
"end": "End",
"relative_end": "Relative End",
"relative_start": "Relative Start",
"start": "Start",
"value": "Value"
},
"content": {
"current_status_accumulated_time": "Current Status Accumulated Time",
"data_unavailable": " There is currently no Lifecycle data for this Job.",
"legend_title": "Legend",
"loading": "Loading Job Timelines....",
"not_available": "N/A",
"previous_status_accumulated_time": "Previous Status Accumulated Time",
"title": "Job Lifecycle Component",
"title_durations": "Historical Status Durations",
"title_loading": "Loading",
"title_transitions": "Transitions"
},
"errors": {
"fetch": "Error getting Job Lifecycle Data"
}
},
"job_payments": {
"buttons": {
"goback": "Go Back",
@@ -1699,6 +1732,7 @@
"ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ",
"calc_repair_days": "Calculated Repair Days",
"calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).",
"calc_scheuled_completion": "Calculate Scheduled Completion",
"cards": {
"customer": "Customer Information",
"damage": "Area of Damage",
@@ -1796,6 +1830,7 @@
"override_header": "Override estimate header on import?",
"ownerassociation": "Owner Association",
"parts": "Parts",
"parts_lines": "Parts Lines",
"parts_received": "Parts Rec.",
"parts_tax_rates": "Parts Tax rates",
"partsfilter": "Parts Only",
@@ -1833,6 +1868,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",
@@ -1856,6 +1892,7 @@
"total_sales": "Total Sales",
"totals": "Totals",
"unvoidnote": "This Job was unvoided.",
"update_scheduled_completion": "Update Scheduled Completion?",
"vehicle_info": "Vehicle",
"vehicleassociation": "Vehicle Association",
"viewallocations": "View Allocations",
@@ -2012,6 +2049,7 @@
"general": "General",
"insurance": "Insurance Information",
"labor": "Labor",
"lifecycle": "Lifecycle",
"partssublet": "Parts & Bills",
"rates": "Rates",
"repairdata": "Repair Data",
@@ -2536,6 +2574,11 @@
"generate": "Generate"
},
"labels": {
"advanced_filters": "Advanced Filters and Sorters",
"advanced_filters_show": "Show",
"advanced_filters_hide": "Hide",
"advanced_filters_filters": "Filters",
"advanced_filters_sorters": "Sorters",
"dates": "Dates",
"employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}",
@@ -2563,6 +2606,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)",
@@ -2919,8 +2963,8 @@
"shop-templates": "Shop Templates | $t(titles.app)",
"shop_vendors": "Vendors | $t(titles.app)",
"techconsole": "Technician Console | $t(titles.app)",
"techjoblookup": "Technician Job Lookup | $t(titles.app)",
"techjobclock": "Technician Job Clock | $t(titles.app)",
"techjoblookup": "Technician Job Lookup | $t(titles.app)",
"techshiftclock": "Technician Shift Clock | $t(titles.app)",
"temporarydocs": "Temporary Documents | $t(titles.app)",
"timetickets": "Time Tickets | $t(titles.app)",

View File

@@ -99,6 +99,7 @@
},
"audit_trail": {
"messages": {
"admin_job_remove_from_ar": "",
"admin_jobmarkexported": "",
"admin_jobmarkforreexport": "",
"admin_jobuninvoice": "",
@@ -112,11 +113,11 @@
"jobassignmentchange": "",
"jobassignmentremoved": "",
"jobchecklist": "",
"jobinvoiced": "",
"jobconverted": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
"jobinvoiced": "",
"jobioucreated": "",
"jobmodifylbradj": "",
"jobnoteadded": "",
@@ -254,13 +255,9 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
"md_functionality_toggles": {
"parts_queue_toggle": ""
},
"appt_colors": {
"color": "",
"label": ""
@@ -334,6 +331,9 @@
"md_ded_notes": "",
"md_email_cc": "",
"md_from_emails": "",
"md_functionality_toggles": {
"parts_queue_toggle": ""
},
"md_hour_split": {
"paint": "",
"prep": ""
@@ -473,6 +473,7 @@
"editaccess": ""
}
},
"ReceivableCustomField": "",
"responsibilitycenter": "",
"responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "",
@@ -814,6 +815,10 @@
"usage": "",
"vehicle": ""
},
"readiness": {
"notready": "",
"ready": ""
},
"status": {
"in": "",
"inservice": "",
@@ -823,10 +828,6 @@
},
"successes": {
"saved": ""
},
"readiness": {
"notready": "",
"ready": ""
}
},
"csi": {
@@ -837,17 +838,24 @@
"creating": "",
"notconfigured": "",
"notfoundsubtitle": "",
"notfoundtitle": ""
"notfoundtitle": "",
"surveycompletetitle": "",
"surveycompletesubtitle": ""
},
"fields": {
"completedon": "",
"created_at": ""
"created_at": "",
"surveyid": "",
"validuntil": ""
},
"labels": {
"nologgedinuser": "",
"nologgedinuser_sub": "",
"noneselected": "",
"title": ""
"title": "",
"greeting": "",
"intro": "",
"copyright": ""
},
"successes": {
"created": "",
@@ -1213,6 +1221,31 @@
"updated": ""
}
},
"job_lifecycle": {
"columns": {
"duration": "",
"end": "",
"relative_end": "",
"relative_start": "",
"start": "",
"value": ""
},
"content": {
"current_status_accumulated_time": "",
"data_unavailable": "",
"legend_title": "",
"loading": "",
"not_available": "",
"previous_status_accumulated_time": "",
"title": "",
"title_durations": "",
"title_loading": "",
"title_transitions": ""
},
"errors": {
"fetch": "Error al obtener los datos del ciclo de vida del trabajo"
}
},
"job_payments": {
"buttons": {
"goback": "",
@@ -1699,6 +1732,7 @@
"ca_gst_all_if_null": "",
"calc_repair_days": "",
"calc_repair_days_tt": "",
"calc_scheuled_completion": "",
"cards": {
"customer": "Información al cliente",
"damage": "Área de Daño",
@@ -1796,6 +1830,7 @@
"override_header": "¿Anular encabezado estimado al importar?",
"ownerassociation": "",
"parts": "Partes",
"parts_lines": "",
"parts_received": "",
"parts_tax_rates": "",
"partsfilter": "",
@@ -1833,6 +1868,7 @@
},
"reconciliationheader": "",
"relatedros": "",
"remove_from_ar": "",
"returntotals": "",
"rosaletotal": "",
"sale_additional": "",
@@ -1856,6 +1892,7 @@
"total_sales": "",
"totals": "",
"unvoidnote": "",
"update_scheduled_completion": "",
"vehicle_info": "Vehículo",
"vehicleassociation": "",
"viewallocations": "",
@@ -2012,6 +2049,7 @@
"general": "",
"insurance": "",
"labor": "Labor",
"lifecycle": "",
"partssublet": "Piezas / Subarrendamiento",
"rates": "",
"repairdata": "Datos de reparación",
@@ -2536,6 +2574,11 @@
"generate": ""
},
"labels": {
"advanced_filters": "",
"advanced_filters_show": "",
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
"dates": "",
"employee": "",
"filterson": "",
@@ -2563,6 +2606,7 @@
},
"templates": {
"anticipated_revenue": "",
"ar_aging": "",
"attendance_detail": "",
"attendance_employee": "",
"attendance_summary": "",
@@ -2919,8 +2963,8 @@
"shop-templates": "",
"shop_vendors": "Vendedores | $t(titles.app)",
"techconsole": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techjobclock": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techshiftclock": "$t(titles.app)",
"temporarydocs": "",
"timetickets": "",

View File

@@ -99,6 +99,7 @@
},
"audit_trail": {
"messages": {
"admin_job_remove_from_ar": "",
"admin_jobmarkexported": "",
"admin_jobmarkforreexport": "",
"admin_jobuninvoice": "",
@@ -112,11 +113,11 @@
"jobassignmentchange": "",
"jobassignmentremoved": "",
"jobchecklist": "",
"jobinvoiced": "",
"jobconverted": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
"jobinvoiced": "",
"jobioucreated": "",
"jobmodifylbradj": "",
"jobnoteadded": "",
@@ -254,7 +255,6 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -331,13 +331,13 @@
"md_ded_notes": "",
"md_email_cc": "",
"md_from_emails": "",
"md_functionality_toggles": {
"parts_queue_toggle": ""
},
"md_hour_split": {
"paint": "",
"prep": ""
},
"md_functionality_toggles": {
"parts_queue_toggle": ""
},
"md_ins_co": {
"city": "",
"name": "",
@@ -473,6 +473,7 @@
"editaccess": ""
}
},
"ReceivableCustomField": "",
"responsibilitycenter": "",
"responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "",
@@ -814,6 +815,10 @@
"usage": "",
"vehicle": ""
},
"readiness": {
"notready": "",
"ready": ""
},
"status": {
"in": "",
"inservice": "",
@@ -823,10 +828,6 @@
},
"successes": {
"saved": ""
},
"readiness": {
"notready": "",
"ready": ""
}
},
"csi": {
@@ -837,17 +838,24 @@
"creating": "",
"notconfigured": "",
"notfoundsubtitle": "",
"notfoundtitle": ""
"notfoundtitle": "",
"surveycompletetitle": "",
"surveycompletesubtitle": ""
},
"fields": {
"completedon": "",
"created_at": ""
"created_at": "",
"surveyid": "",
"validuntil": ""
},
"labels": {
"nologgedinuser": "",
"nologgedinuser_sub": "",
"noneselected": "",
"title": ""
"title": "",
"greeting": "",
"intro": "",
"copyright": ""
},
"successes": {
"created": "",
@@ -1213,6 +1221,31 @@
"updated": ""
}
},
"job_lifecycle": {
"columns": {
"duration": "",
"end": "",
"relative_end": "",
"relative_start": "",
"start": "",
"value": ""
},
"content": {
"current_status_accumulated_time": "",
"data_unavailable": "",
"legend_title": "",
"loading": "",
"not_available": "",
"previous_status_accumulated_time": "",
"title": "",
"title_durations": "",
"title_loading": "",
"title_transitions": ""
},
"errors": {
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
}
},
"job_payments": {
"buttons": {
"goback": "",
@@ -1699,6 +1732,7 @@
"ca_gst_all_if_null": "",
"calc_repair_days": "",
"calc_repair_days_tt": "",
"calc_scheuled_completion": "",
"cards": {
"customer": "Informations client",
"damage": "Zone de dommages",
@@ -1796,6 +1830,7 @@
"override_header": "Remplacer l'en-tête d'estimation à l'importation?",
"ownerassociation": "",
"parts": "les pièces",
"parts_lines": "",
"parts_received": "",
"parts_tax_rates": "",
"partsfilter": "",
@@ -1833,6 +1868,7 @@
},
"reconciliationheader": "",
"relatedros": "",
"remove_from_ar": "",
"returntotals": "",
"rosaletotal": "",
"sale_additional": "",
@@ -1856,6 +1892,7 @@
"total_sales": "",
"totals": "",
"unvoidnote": "",
"update_scheduled_completion": "",
"vehicle_info": "Véhicule",
"vehicleassociation": "",
"viewallocations": "",
@@ -2012,6 +2049,7 @@
"general": "",
"insurance": "",
"labor": "La main d'oeuvre",
"lifecycle": "",
"partssublet": "Pièces / Sous-location",
"rates": "",
"repairdata": "Données de réparation",
@@ -2536,6 +2574,11 @@
"generate": ""
},
"labels": {
"advanced_filters": "",
"advanced_filters_show": "",
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
"dates": "",
"employee": "",
"filterson": "",
@@ -2563,6 +2606,7 @@
},
"templates": {
"anticipated_revenue": "",
"ar_aging": "",
"attendance_detail": "",
"attendance_employee": "",
"attendance_summary": "",
@@ -2919,8 +2963,8 @@
"shop-templates": "",
"shop_vendors": "Vendeurs | $t(titles.app)",
"techconsole": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techjobclock": "$t(titles.app)",
"techjoblookup": "$t(titles.app)",
"techshiftclock": "$t(titles.app)",
"temporarydocs": "",
"timetickets": "",

View File

@@ -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;

View File

@@ -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")

View File

@@ -12,6 +12,8 @@ import apolloLogger from "apollo-link-logger";
//import axios from "axios";
import { auth } from "../firebase/firebase.utils";
import errorLink from "../graphql/apollo-error-handling";
import { SentryLink } from "apollo-link-sentry";
//import { store } from "../redux/store";
const httpLink = new HttpLink({
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
@@ -105,18 +107,30 @@ const link = split(
const authLink = setContext((_, { headers }) => {
return (
auth.currentUser &&
auth.currentUser.getIdToken().then((token) => {
if (token) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
} else {
auth.currentUser
.getIdToken()
.then((token) => {
if (token) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
} else {
console.error(
"Authentication error. Unable to add authorization token because it was empty."
);
return { headers };
}
})
.catch((error) => {
console.error(
"Authentication error. Unable to add authorization token.",
error.message
);
return { headers };
}
})
})
);
});
@@ -138,8 +152,10 @@ if (process.env.NODE_ENV === "development") {
}
middlewares.push(
roundTripLink.concat(
retryLink.concat(errorLink.concat(authLink.concat(link)))
new SentryLink().concat(
roundTripLink.concat(
retryLink.concat(errorLink.concat(authLink.concat(link)))
)
)
);

View File

@@ -1,14 +1,16 @@
import { gql } from "@apollo/client";
import {gql} from "@apollo/client";
import jsreport from "@jsreport/browser-client";
import { notification } from "antd";
import {notification} from "antd";
import axios from "axios";
import _ from "lodash";
import { auth } from "../firebase/firebase.utils";
import { setEmailOptions } from "../redux/email/email.actions";
import { store } from "../redux/store";
import {auth} from "../firebase/firebase.utils";
import {setEmailOptions} from "../redux/email/email.actions";
import {store} from "../redux/store";
import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios";
import { TemplateList } from "./TemplateConstants";
import {TemplateList} from "./TemplateConstants";
import {applyFilters, applySorters, parseQuery, printQuery, wrapFiltersInAnd} from "./graphQLmodifier";
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
jsreport.serverUrl = server;
@@ -16,11 +18,11 @@ jsreport.serverUrl = server;
const Templates = TemplateList();
export default async function RenderTemplate(
templateObject,
bodyshop,
renderAsHtml = false,
renderAsExcel = false,
renderAsText = false
templateObject,
bodyshop,
renderAsHtml = false,
renderAsExcel = false,
renderAsText = false
) {
if (window.jsr3) {
jsreport.serverUrl = "https://reports3.test.imex.online/";
@@ -30,41 +32,41 @@ export default async function RenderTemplate(
jsreport.headers["Authorization"] = jsrAuth;
//Query assets that match the template name. Must be in format <<templateName>>.query
let { contextData, useShopSpecificTemplate } = await fetchContextData(
templateObject,
jsrAuth
let {contextData, useShopSpecificTemplate} = await fetchContextData(
templateObject,
jsrAuth
);
const { ignoreCustomMargins } = Templates[templateObject.name];
const {ignoreCustomMargins} = Templates[templateObject.name];
let reportRequest = {
template: {
name: useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${templateObject.name}`
: `/${templateObject.name}`,
? `/${bodyshop.imexshopid}/${templateObject.name}`
: `/${templateObject.name}`,
...(renderAsHtml
? {}
: {
? {}
: {
recipe: "chrome-pdf",
...(!ignoreCustomMargins && {
chrome: {
marginTop:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
marginBottom:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px",
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px",
},
}),
}),
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
...(renderAsText ? { recipe: "text" } : {}),
...(renderAsExcel ? {recipe: "html-to-xlsx"} : {}),
...(renderAsText ? {recipe: "text"} : {}),
},
data: {
...contextData,
@@ -73,7 +75,7 @@ export default async function RenderTemplate(
headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop,
offset: bodyshop.timezone, //moment().utcOffset(),
offset: bodyshop.timezone, //dayjs().utcOffset(),
},
};
@@ -82,8 +84,8 @@ export default async function RenderTemplate(
if (!renderAsHtml) {
render.download(
(Templates[templateObject.name] &&
Templates[templateObject.name].title) ||
(Templates[templateObject.name] &&
Templates[templateObject.name].title) ||
""
);
} else {
@@ -97,17 +99,17 @@ export default async function RenderTemplate(
...(!ignoreCustomMargins && {
chrome: {
marginTop:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
marginBottom:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px",
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px",
},
}),
},
@@ -121,21 +123,21 @@ export default async function RenderTemplate(
resolve({
pdf,
filename:
Templates[templateObject.name] &&
Templates[templateObject.name].title,
Templates[templateObject.name] &&
Templates[templateObject.name].title,
html,
});
});
}
} catch (error) {
notification["error"]({ message: JSON.stringify(error) });
notification["error"]({message: JSON.stringify(error)});
}
}
export async function RenderTemplates(
templateObjects,
bodyshop,
renderAsHtml = false
templateObjects,
bodyshop,
renderAsHtml = false
) {
//Query assets that match the template name. Must be in format <<templateName>>.query
let unsortedTemplatesAndData = [];
@@ -145,17 +147,17 @@ export async function RenderTemplates(
templateObjects.forEach((template) => {
proms.push(
(async () => {
let { contextData, useShopSpecificTemplate } = await fetchContextData(
template,
jsrAuth
);
unsortedTemplatesAndData.push({
templateObject: template,
contextData,
useShopSpecificTemplate,
});
})()
(async () => {
let {contextData, useShopSpecificTemplate} = await fetchContextData(
template,
jsrAuth
);
unsortedTemplatesAndData.push({
templateObject: template,
contextData,
useShopSpecificTemplate,
});
})()
);
});
await Promise.all(proms);
@@ -172,8 +174,8 @@ export async function RenderTemplates(
unsortedTemplatesAndData.sort(function (a, b) {
return (
templateObjects.findIndex((x) => x.name === a.templateObject.name) -
templateObjects.findIndex((x) => x.name === b.templateObject.name)
templateObjects.findIndex((x) => x.name === a.templateObject.name) -
templateObjects.findIndex((x) => x.name === b.templateObject.name)
);
});
const templateAndData = unsortedTemplatesAndData;
@@ -183,25 +185,25 @@ export async function RenderTemplates(
let reportRequest = {
template: {
name: rootTemplate.useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
: `/${rootTemplate.templateObject.name}`,
? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
: `/${rootTemplate.templateObject.name}`,
...(renderAsHtml
? {}
: {
? {}
: {
recipe: "chrome-pdf",
chrome: {
marginTop:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
marginBottom:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px",
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px",
},
}),
pdfOperations: [
@@ -218,22 +220,22 @@ export async function RenderTemplates(
template: {
chrome: {
marginTop:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
marginBottom:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px",
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px",
},
name: template.useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${template.templateObject.name}`
: `/${template.templateObject.name}`,
...(renderAsHtml ? {} : { recipe: "chrome-pdf" }),
? `/${bodyshop.imexshopid}/${template.templateObject.name}`
: `/${template.templateObject.name}`,
...(renderAsHtml ? {} : {recipe: "chrome-pdf"}),
},
type: "append",
@@ -245,8 +247,8 @@ export async function RenderTemplates(
},
data: {
...extend(
rootTemplate.contextData,
...templateAndData.map((temp) => temp.contextData)
rootTemplate.contextData,
...templateAndData.map((temp) => temp.contextData)
),
// ...rootTemplate.templateObject.variables,
@@ -266,32 +268,31 @@ export async function RenderTemplates(
return render.toString();
}
} catch (error) {
notification["error"]({ message: JSON.stringify(error) });
notification["error"]({message: JSON.stringify(error)});
}
}
export const GenerateDocument = async (
template,
messageOptions,
sendType,
jobid
template,
messageOptions,
sendType,
jobid
) => {
const bodyshop = store.getState().user.bodyshop;
if (sendType === "e") {
store.dispatch(
setEmailOptions({
jobid,
messageOptions: {
...messageOptions,
to: Array.isArray(messageOptions.to)
? messageOptions.to
: [messageOptions.to],
},
template,
})
setEmailOptions({
jobid,
messageOptions: {
...messageOptions,
to: Array.isArray(messageOptions.to)
? messageOptions.to
: [messageOptions.to],
},
template,
})
);
} else if (sendType === "x") {
console.log("excel");
await RenderTemplate(template, bodyshop, false, true);
} else if (sendType === "text") {
await RenderTemplate(template, bodyshop, false, false, true);
@@ -305,22 +306,74 @@ export const GenerateDocuments = async (templates) => {
await RenderTemplates(templates, bodyshop);
};
export const fetchFilterData = async ({name}) => {
try {
const bodyshop = store.getState().user.bodyshop;
const jsrAuth = (await axios.post("/utils/jsr")).data;
jsreport.headers["FirebaseAuthorization"] =
"Bearer " + (await auth.currentUser.getIdToken());
const folders = await cleanAxios.get(`${server}/odata/folders`, {
headers: {Authorization: jsrAuth},
});
const shopSpecificFolder = folders.data.value.find(
(f) => f.name === bodyshop.imexshopid
);
const jsReportFilters = await cleanAxios.get(
`${server}/odata/assets?$filter=name eq '${name}.filters'`,
{headers: {Authorization: jsrAuth}}
);
console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
let parsedFilterData;
let useShopSpecificTemplate = false;
// let shopSpecificTemplate;
if (shopSpecificFolder) {
let shopSpecificTemplate = jsReportFilters.data.value.find(
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
);
if (shopSpecificTemplate) {
useShopSpecificTemplate = true;
parsedFilterData = atob(shopSpecificTemplate.content);
}
}
if (!parsedFilterData) {
const generalTemplate = jsReportFilters.data.value.find((f) => !f.folder);
useShopSpecificTemplate = false;
if (generalTemplate) parsedFilterData = atob(generalTemplate.content);
}
const data = JSON.parse(parsedFilterData);
return {
data,
useShopSpecificTemplate,
success: true,
}
} catch {
return {
success: false,
}
}
};
const fetchContextData = async (templateObject, jsrAuth) => {
const bodyshop = store.getState().user.bodyshop;
jsreport.headers["FirebaseAuthorization"] =
"Bearer " + (await auth.currentUser.getIdToken());
"Bearer " + (await auth.currentUser.getIdToken());
const folders = await cleanAxios.get(`${server}/odata/folders`, {
headers: { Authorization: jsrAuth },
headers: {Authorization: jsrAuth},
});
const shopSpecificFolder = folders.data.value.find(
(f) => f.name === bodyshop.imexshopid
(f) => f.name === bodyshop.imexshopid
);
const jsReportQueries = await cleanAxios.get(
`${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
{ headers: { Authorization: jsrAuth } }
`${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
{headers: {Authorization: jsrAuth}}
);
let templateQueryToExecute;
@@ -329,7 +382,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
if (shopSpecificFolder) {
let shopSpecificTemplate = jsReportQueries.data.value.find(
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
);
if (shopSpecificTemplate) {
useShopSpecificTemplate = true;
@@ -343,16 +396,57 @@ const fetchContextData = async (templateObject, jsrAuth) => {
templateQueryToExecute = atob(generalTemplate.content);
}
// Commented out for future revision debugging
// console.log('Template Object');
// console.dir(templateObject);
// console.log('Unmodified Query');
// console.dir(templateQueryToExecute);
// We have no template filters or sorters, so we can just execute the query and return the data
if ((!templateObject?.filters && !templateObject?.filters?.length && !templateObject?.sorters && !templateObject?.sorters?.length)) {
let contextData = {};
if (templateQueryToExecute) {
const {data} = await client.query({
query: gql(templateQueryToExecute),
variables: {...templateObject.variables},
});
contextData = data;
}
return {contextData, useShopSpecificTemplate};
}
// Parse the query and apply the filters and sorters
const ast = parseQuery(templateQueryToExecute);
let filterFields = [];
if (templateObject?.filters && templateObject?.filters?.length) {
applyFilters(ast, templateObject.filters, filterFields);
wrapFiltersInAnd(ast, filterFields);
}
if (templateObject?.sorters && templateObject?.sorters?.length) {
applySorters(ast, templateObject.sorters);
}
const finalQuery = printQuery(ast);
// commented out for future revision debugging
// console.log('Modified Query');
// console.log(finalQuery);
let contextData = {};
if (templateQueryToExecute) {
const { data } = await client.query({
query: gql(templateQueryToExecute),
variables: { ...templateObject.variables },
const {data} = await client.query({
query: gql(finalQuery),
variables: {...templateObject.variables},
});
contextData = data;
}
return { contextData, useShopSpecificTemplate };
return {contextData, useShopSpecificTemplate};
};
//export const displayTemplateInWindow = (html) => {
@@ -389,7 +483,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
function extend(o1, o2, o3) {
var result = {},
obj;
obj;
for (var i = 0; i < arguments.length; i++) {
obj = arguments[i];
@@ -405,4 +499,4 @@ function extend(o1, o2, o3) {
}
}
return result;
}
}

View File

@@ -2039,6 +2039,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"

View File

@@ -0,0 +1,309 @@
import {Kind, parse, print, visit} from "graphql";
const STRING_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_like", label: "contains"},
{value: "_nlike", label: "does not contain"},
{value: "_ilike", label: "contains case-insensitive"},
{value: "_nilike", label: "does not contain case-insensitive"}
];
const NUMBER_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"}
];
export function getOperatorsByType(type = 'string') {
const operators = {
string: STRING_OPERATORS,
number: NUMBER_OPERATORS
};
return operators[type];
}
/* eslint-disable no-loop-func */
/**
* Parse a GraphQL query into an AST
* @param query
* @returns {DocumentNode}
*/
export function parseQuery(query) {
return parse(query);
}
/**
* Print an AST back into a GraphQL query
* @param query
* @returns {string}
*/
export function printQuery(query) {
return print(query);
}
/**
* Apply sorters to the AST
* @param ast
* @param sorters
*/
export function applySorters(ast, sorters) {
sorters.forEach((sorter) => {
const fieldPath = sorter.field.split('.');
visit(ast, {
OperationDefinition: {
enter(node) {
// Loop through each sorter to apply it
// noinspection DuplicatedCode
let currentSelection = node; // Start with the root operation
// Navigate down the field path to the correct location
for (let i = 0; i < fieldPath.length - 1; i++) {
let found = false;
visit(currentSelection, {
Field: {
enter(node) {
if (node.name.value === fieldPath[i]) {
currentSelection = node; // Move down to the next level
found = true;
}
}
}
});
if (!found) break; // Stop if we can't find the next field in the path
}
// Apply the sorter at the correct level
if (currentSelection) {
const targetFieldName = fieldPath[fieldPath.length - 1];
let orderByArg = currentSelection.arguments.find(arg => arg.name.value === 'order_by');
if (!orderByArg) {
orderByArg = {
kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'order_by' },
value: { kind: Kind.OBJECT, fields: [] },
};
currentSelection.arguments.push(orderByArg);
}
const sorterField = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetFieldName },
value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions
};
// Add the new sorter condition
orderByArg.value.fields.push(sorterField);
}
}
}
});
});
}
/**
* Apply filters to the AST
* @param ast
* @param filters
*/
export function applyFilters(ast, filters) {
return visit(ast, {
OperationDefinition: {
enter(node) {
filters.forEach(filter => {
const fieldPath = filter.field.split('.');
let topLevel = false;
// Determine if the filter should be applied at the top level
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevel = true;
}
if (topLevel) {
// Construct the filter for a top-level application
const targetFieldName = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
const nestedFilter = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetFieldName },
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: filter.operator },
value: filterValue,
}],
},
};
// Find or create the where argument for the top-level field
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'where' },
value: { kind: Kind.OBJECT, fields: [] },
};
const topLevelSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: path },
value: { kind: Kind.OBJECT, fields: [] }
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(nestedFilter);
} else { // Directly under the top level
whereArg.value.fields.push(nestedFilter);
}
} else {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
let whereArgFound = false;
// Iterate over the fieldPath, except for the last entry, to navigate the structure
for (let i = 0; i < fieldPath.length - 1; i++) {
const fieldName = fieldPath[i];
let fieldFound = false;
// Check if the current selection has a selectionSet and selections
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
// Look for the field in the current selection's selections
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
if (selection) {
// Move down the AST to the found selection
currentSelection = selection;
fieldFound = true;
}
}
// If the field was not found in the current path, it's an issue
if (!fieldFound) {
console.error(`Field ${fieldName} not found in the current selection.`);
return; // Exit the loop and function due to error
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied
// Check if the 'where' argument already exists in the current selection
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
if (whereArg) {
whereArgFound = true;
} else {
// If not found, create a new 'where' argument for the current selection
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'where' },
value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter
});
}
// Assuming the last entry in fieldPath is the field to apply the filter on
const targetField = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
// Construct the filter field object
const filterField = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetField },
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: filter.operator },
value: filterValue,
}],
},
};
// Add the filter field to the 'where' clause of the current selection
if (whereArgFound) {
whereArg.value.fields.push(filterField);
} else {
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
}
}
});
}
}
});
}
/**
* Get the GraphQL kind for a value
* @param value
* @returns {Kind|Kind.INT}
*/
function getGraphQLKind(value) {
if (typeof value === 'number') {
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
} else if (typeof value === 'boolean') {
return Kind.BOOLEAN;
} else if (typeof value === 'string') {
return Kind.STRING;
}
// Extend with more types as needed
}
/**
* Wrap filters in an 'and' object
* @param ast
* @param filterFields
*/
export function wrapFiltersInAnd(ast, filterFields) {
visit(ast, {
OperationDefinition: {
enter(node) {
node.selectionSet.selections.forEach((selection) => {
let whereArg = selection.arguments.find(arg => arg.name.value === 'where');
if (filterFields.length > 1) {
const andFilter = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: '_and'},
value: {kind: Kind.LIST, values: filterFields}
};
whereArg.value.fields.push(andFilter);
} else if (filterFields.length === 1) {
whereArg.value.fields.push(filterFields[0].fields[0]);
}
});
}
}
});
}
/* eslint-enable no-loop-func */

View File

@@ -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,6 +4189,11 @@
- name: job_status_transition
definition:
enable_manual: true
insert:
columns: '*'
update:
columns:
- status
retry_conf:
interval_sec: 10
num_retries: 0

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" rename column "business_name" to "buisness_name";

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" rename column "buisness_name" to "business_name";

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" alter column "phone_number" set not null;

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" alter column "phone_number" drop not null;

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" alter column "address" set not null;

View File

@@ -0,0 +1 @@
alter table "public"."eula_acceptances" alter column "address" drop not null;

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
alter table "public"."job_ar_schema" add column "shopid" uuid
null;

View File

@@ -0,0 +1 @@
alter table "public"."job_ar_schema" drop constraint "job_ar_schema_id_fkey";

View File

@@ -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;

View File

@@ -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$;

View File

@@ -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$;

View File

@@ -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$;

View File

@@ -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$;

View File

@@ -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$;

View File

@@ -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$;

File diff suppressed because it is too large Load Diff

411
package-lock.json generated
View File

@@ -41,6 +41,7 @@
"node-quickbooks": "^2.0.43",
"nodemailer": "^6.9.7",
"phone": "^3.1.41",
"rimraf": "^5.0.5",
"soap": "^1.0.0",
"socket.io": "^4.7.2",
"ssh2-sftp-client": "^9.1.0",
@@ -939,6 +940,95 @@
"node": ">=6"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@jonkemp/package-utils": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz",
@@ -1016,6 +1106,15 @@
"yarn": "^1.22.10"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -1897,7 +1996,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"devOptional": true,
"engines": {
"node": ">=8"
}
@@ -1906,7 +2004,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"devOptional": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -2041,8 +2138,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"devOptional": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -2148,7 +2244,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"devOptional": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -2346,7 +2441,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"devOptional": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -2650,6 +2744,19 @@
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/csrf": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
@@ -2954,6 +3061,11 @@
"stream-shift": "^1.0.0"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -2994,8 +3106,7 @@
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"devOptional": true
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/enabled": {
"version": "2.0.0",
@@ -3507,6 +3618,21 @@
}
}
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -4210,7 +4336,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"devOptional": true,
"engines": {
"node": ">=8"
}
@@ -4254,11 +4379,33 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jake": {
"version": "10.8.7",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
@@ -4842,6 +4989,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -5209,6 +5364,37 @@
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
"dependencies": {
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
"integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@@ -5670,57 +5856,55 @@
}
},
"node_modules/rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"dev": true,
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
"integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
"dependencies": {
"glob": "^7.1.3"
"glob": "^10.3.7"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
"rimraf": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.5",
"minimatch": "^9.0.1",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
"path-scurry": "^1.10.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "*"
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dependencies": {
"brace-expansion": "^1.1.7"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "*"
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rndm": {
@@ -5869,6 +6053,25 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"engines": {
"node": ">=8"
}
},
"node_modules/shell-quote": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
@@ -5891,6 +6094,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -6313,7 +6527,20 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"devOptional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -6335,7 +6562,18 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"devOptional": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -6523,6 +6761,60 @@
"node": ">=6.0.0"
}
},
"node_modules/temp/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/temp/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/temp/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/temp/node_modules/rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/text-decoding": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz",
@@ -6922,6 +7214,20 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/winston": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz",
@@ -6981,6 +7287,23 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@@ -8,6 +8,7 @@
},
"scripts": {
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
"admin": "cd admin && npm start",
"client": "cd client && npm start",
"server": "nodemon server.js",
@@ -49,6 +50,7 @@
"node-quickbooks": "^2.0.43",
"nodemailer": "^6.9.7",
"phone": "^3.1.41",
"rimraf": "^5.0.5",
"soap": "^1.0.0",
"socket.io": "^4.7.2",
"ssh2-sftp-client": "^9.1.0",

344
server.js
View File

@@ -1,299 +1,95 @@
// 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");
// 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: {
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");
// app.set('trust proxy', true)
// app.use(fb.validateFirebaseIdToken);
// 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(bodyParser.json({limit: "50mb"}));
app.use(bodyParser.urlencoded({limit: "50mb", extended: true}));
app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]}));
//Email Based Paths.
var 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}`);
// Helper middleware
app.use((req, res, next) => {
req.logger = logger;
next();
});
//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
);
// Route groupings
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"));
app.use('/csi', require("./server/routes/csiRoutes"));
//Cloudinary Media Paths
var 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
var smsReceive = require("./server/sms/receive");
app.post(
"/sms/receive",
twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }),
smsReceive.receive
);
var smsSend = require("./server/sms/send");
app.post("/sms/send", fb.validateFirebaseIdToken, smsSend.send);
var 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
);
var 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.post("/job/costingmulti", fb.validateFirebaseIdToken, job.costingmulti);
var partsScan = require("./server/parts-scan/parts-scan");
app.post("/job/partsscan", fb.validateFirebaseIdToken, partsScan.partsScan);
//Scheduling
var 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");
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
var tech = require("./server/tech/tech");
app.post("/tech/login", fb.validateFirebaseIdToken, tech.techLogin);
var 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");
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");
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");
app.post("/taskHandler", fb.validateFirebaseIdToken, taskHandler.taskHandler);
var mixdataUpload = require("./server/mixdata/mixdata");
app.post(
"/mixdata/upload",
fb.validateFirebaseIdToken,
upload.any(),
mixdataUpload.mixdataUpload
);
var 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
);
var 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");
app.post(
"/opensearch", //fb.validateFirebaseIdToken,
os.handler
);
app.post("/search", fb.validateFirebaseIdToken, os.search);
var 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.");
// Default route for forbidden access
app.get("/", (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"
);
});
const main = async () => {
await server.listen(port);
}
// Start server
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);
});

View File

@@ -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(

View File

@@ -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, {

View File

@@ -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, {

View File

@@ -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, {

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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 })

2
server/csi/csi.js Normal file
View File

@@ -0,0 +1,2 @@
exports.lookup = require("./lookup").default;
exports.submit = require("./submit").default;

24
server/csi/lookup.js Normal file
View File

@@ -0,0 +1,24 @@
const path = require("path");
const queries = require("../graphql-client/queries");
const logger = require("../utils/logger");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const client = require("../graphql-client/graphql-client").client;
exports.default = async (req, res) => {
try {
logger.log("csi-surveyID-lookup", "DEBUG", "csi", req.body.surveyId, null);
const gql_response = await client.request(queries.QUERY_SURVEY, {
surveyId: req.body.surveyId,
});
res.status(200).json(gql_response);
} catch (error) {
logger.log("csi-surveyID-lookup", "ERROR", "csi", req.body.surveyId, error);
res.status(400).json(error);
}
};

29
server/csi/submit.js Normal file
View File

@@ -0,0 +1,29 @@
const path = require("path");
const queries = require("../graphql-client/queries");
const logger = require("../utils/logger");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const client = require("../graphql-client/graphql-client").client;
exports.default = async (req, res) => {
try {
logger.log("csi-surveyID-submit", "DEBUG", "csi", req.body.surveyId, null);
const gql_response = await client.request(queries.COMPLETE_SURVEY, {
surveyId: req.body.surveyId,
survey: {
response: req.body.values,
valid: false,
completedon: new Date(),
},
});
res.status(200).json(gql_response);
} catch (error) {
logger.log("csi-surveyID-submit", "ERROR", "csi", req.body.surveyId, error);
res.status(400).json(error);
}
};

View File

@@ -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} <noreply@imex.online>`,
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} <noreply@imex.online>`,
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 <noreply@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 <noreply@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 <noreply@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 <noreply@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);
};

View File

@@ -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";

View File

@@ -518,6 +518,21 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
}
}`;
exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) {
transitions(where: {jobid: {_in: $jobids}}, order_by: {end: desc}) {
start
end
value
prev_value
next_value
duration
type
created_at
updated_at
jobid
}
}`;
exports.QUERY_UPCOMING_APPOINTMENTS = `query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
jobs_by_pk(id: $jobId) {
bodyshop {
@@ -1974,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
}`
: ""
}
}`;
@@ -2133,3 +2156,23 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
shopid
}
}`;
exports.QUERY_SURVEY = `query QUERY_SURVEY($surveyId: uuid!) {
csi_by_pk(id: $surveyId) {
completedon
csiquestion {
id
config
}
id
relateddata
valid
validuntil
}
}`;
exports.COMPLETE_SURVEY = `mutation COMPLETE_SURVEY($surveyId: uuid!, $survey: csi_set_input) {
update_csi(where: { id: { _eq: $surveyId } }, _set: $survey) {
affected_rows
}
}`;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
const _ = require("lodash");
const queries = require("../graphql-client/queries");
const moment = require("moment");
const durationToHumanReadable = require("../utils/durationToHumanReadable");
const calculateStatusDuration = require("../utils/calculateStatusDuration");
const jobLifecycle = async (req, res) => {
// Grab the jobids and statuses from the request body
const {
jobids,
statuses
} = req.body;
if (!jobids) {
return res.status(400).json({
error: "Missing jobids"
});
}
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;
if (!transitions) {
return res.status(200).json({
jobIDs,
transitions: []
});
}
const transitionsByJobId = _.groupBy(resp.transitions, 'jobid');
const groupedTransitions = {};
for (let jobId in transitionsByJobId) {
let lifecycle = transitionsByJobId[jobId].map(transition => {
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;
});
groupedTransitions[jobId] = {
lifecycle: lifecycle,
durations: calculateStatusDuration(lifecycle, statuses),
};
}
return res.status(200).json({
jobIDs,
transition: groupedTransitions,
});
}
module.exports = jobLifecycle;

View File

@@ -9,24 +9,25 @@ 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"}`
),
});
async function StatusTransition(req, res) {
if (req.headers["event-secret"] !== process.env.EVENT_SECRET) {
res.status(401).send("Unauthorized");
return;
}
res.sendStatus(200);
return;
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,
@@ -47,27 +48,36 @@ async function StatusTransition(req, res) {
: 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(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",
},
}
);
logger.log("job-transition-update-result", "DEBUG", null, jobid, resp2);
//Check to see if there is an existing status transition record.
//Query using Job ID, start is not null, end is null.

View File

@@ -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),

View File

@@ -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');

View File

@@ -0,0 +1,20 @@
const path = require("path");
/**
* 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
*/
function eventAuthorizationMiddleware(req, res, next) {
if (req.headers["event-secret"] !== process.env.EVENT_SECRET) {
return res.status(401).send("Unauthorized");
}
req.isEventAuthorized = true;
next();
}
module.exports = eventAuthorizationMiddleware;

View File

@@ -0,0 +1,26 @@
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, {
request: req.body,
user: req.user,
});
return res.sendStatus(404);
}
req.isAdmin = true;
next();
};
module.exports = validateAdminMiddleware;

View File

@@ -0,0 +1,69 @@
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<void>}
*/
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.");
return res.status(403).send("Unauthorized");
}
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",
});
return res.status(403).send("Unauthorized");
}
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,
});
return res.status(401).send("Unauthorized");
}
};
module.exports = validateFirebaseIdTokenMiddleware;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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");
@@ -15,10 +14,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();
@@ -186,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})

View File

@@ -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.

View File

@@ -0,0 +1,13 @@
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', withUserGraphQLClientMiddleware, receivables);
router.post('/qbxml/payables', withUserGraphQLClientMiddleware, payables);
router.post('/qbxml/payments', withUserGraphQLClientMiddleware, payments);
module.exports = router;

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