Compare commits
310 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c49fa1c527 | ||
|
|
d29ffc21e5 | ||
|
|
e47731702a | ||
|
|
85a3aeb335 | ||
|
|
80b7ae0e54 | ||
|
|
0529ac4478 | ||
|
|
3cafbebbee | ||
|
|
6f248d864e | ||
|
|
a4a84572b7 | ||
|
|
a45d0bb9f4 | ||
|
|
a2e0f9fbe7 | ||
|
|
e37dc0a18f | ||
|
|
0bc00d46cf | ||
|
|
e80e40bb76 | ||
|
|
a88c102b27 | ||
|
|
c691d44c44 | ||
|
|
ebb3a13ff5 | ||
|
|
3846b7c5fc | ||
|
|
93d139f926 | ||
|
|
4d1f40537c | ||
|
|
896f1415f7 | ||
|
|
8a01cd9cb0 | ||
|
|
c2013d47e7 | ||
|
|
0f20807690 | ||
|
|
2a45be6a45 | ||
|
|
b63602143e | ||
|
|
2add712270 | ||
|
|
77c8f74bcb | ||
|
|
1d38102541 | ||
|
|
6117b5ab64 | ||
|
|
578f0a110e | ||
|
|
b5c66274ca | ||
|
|
be46bdc57f | ||
|
|
6263e63a1d | ||
|
|
83d702f12b | ||
|
|
37708a0b59 | ||
|
|
6cfcab8156 | ||
|
|
33ec18986d | ||
|
|
6b7b34ae79 | ||
|
|
06ef2482ba | ||
|
|
83bd485597 | ||
|
|
6921f2fe68 | ||
|
|
a7e199932c | ||
|
|
2427c14b7b | ||
|
|
66655d449e | ||
|
|
845a84c4c8 | ||
|
|
0ff5ea3d59 | ||
|
|
09492e647e | ||
|
|
2d6594cc73 | ||
|
|
3b8e83d88a | ||
|
|
3ec4dbb5b8 | ||
|
|
601fdbba39 | ||
|
|
830adc4ef3 | ||
|
|
3f216195ca | ||
|
|
bcb8de0937 | ||
|
|
9cc0d6175e | ||
|
|
4c07632ee6 | ||
|
|
767c219af8 | ||
|
|
cfc301570e | ||
|
|
fb718f9f37 | ||
|
|
cafc0e5628 | ||
|
|
a635725839 | ||
|
|
eb8e9b10ef | ||
|
|
2584f7129c | ||
|
|
6c996037d6 | ||
|
|
f421b45222 | ||
|
|
30cf46a158 | ||
|
|
8d37c54f89 | ||
|
|
f9b9f39418 | ||
|
|
05a5df789b | ||
|
|
bed87eda97 | ||
|
|
67008c35b8 | ||
|
|
9b95e40a13 | ||
|
|
e3a998b6f8 | ||
|
|
3c7ede0155 | ||
|
|
82c7aa1347 | ||
|
|
4ccd912363 | ||
|
|
c0525842fd | ||
|
|
dd5ca5d233 | ||
|
|
616a4b04a0 | ||
|
|
9a34640c88 | ||
|
|
3110be4703 | ||
|
|
971c8d41ff | ||
|
|
7c303a5154 | ||
|
|
9383b37a41 | ||
|
|
205d507097 | ||
|
|
97a1bd66d1 | ||
|
|
0d1ff6390c | ||
|
|
830d2c87d2 | ||
|
|
da98e7b886 | ||
|
|
a74a9ba5a1 | ||
|
|
b8a3081488 | ||
|
|
c8fc1b0f68 | ||
|
|
da1ddb874f | ||
|
|
771f9ceaa8 | ||
|
|
9b61da5c62 | ||
|
|
7daf7540b3 | ||
|
|
ce6940629d | ||
|
|
b706b96d32 | ||
|
|
2427bc72f2 | ||
|
|
8bc1a9d9ee | ||
|
|
ba30225ba1 | ||
|
|
cb4a6e8774 | ||
|
|
23f640028d | ||
|
|
3e5e6263fe | ||
|
|
48cef3e188 | ||
|
|
8c0d6b2f6b | ||
|
|
22ee8acd0d | ||
|
|
afdcfb7bf6 | ||
|
|
c1f6d06128 | ||
|
|
120a8a4576 | ||
|
|
89224e871c | ||
|
|
fa0d472fb6 | ||
|
|
b0f4ad7e4f | ||
|
|
7503d86c69 | ||
|
|
efd1c17033 | ||
|
|
c7a0072f2d | ||
|
|
908942ec09 | ||
|
|
e4d3b53349 | ||
|
|
03ce5458b5 | ||
|
|
61c03ee206 | ||
|
|
0e4f5b8b2a | ||
|
|
b5d4944ad8 | ||
|
|
50f84d40e1 | ||
|
|
a394d6b37e | ||
|
|
f8408908b2 | ||
|
|
eb8519dc1d | ||
|
|
36dd97394f | ||
|
|
03d4e4dcd1 | ||
|
|
6489a8666f | ||
|
|
5ea64ed805 | ||
|
|
d740446ccb | ||
|
|
d0a2bb7da0 | ||
|
|
5de4ef5d83 | ||
|
|
f59bdf9030 | ||
|
|
cfe0727447 | ||
|
|
09d112350a | ||
|
|
52f8eabd2b | ||
|
|
a162b275a3 | ||
|
|
2e7232bb65 | ||
|
|
82dc9e1c56 | ||
|
|
272a3f579a | ||
|
|
ff1ceb20cb | ||
|
|
343179d4fe | ||
|
|
eabbc2211b | ||
|
|
7f587680ca | ||
|
|
d61ed03ef1 | ||
|
|
166efdc877 | ||
|
|
9274742520 | ||
|
|
9b4c85c9e3 | ||
|
|
66c64ce9e0 | ||
|
|
c9cbffdec8 | ||
|
|
ca3145ce0f | ||
|
|
52f9106776 | ||
|
|
1a2cc5623d | ||
|
|
d06037df1f | ||
|
|
cb8632641e | ||
|
|
adbfcddd9d | ||
|
|
e3aea55e91 | ||
|
|
bf6b1c202f | ||
|
|
0cab47f984 | ||
|
|
829e611692 | ||
|
|
e0e62a52be | ||
|
|
23c0f8e383 | ||
|
|
764a89e5bf | ||
|
|
f4908ed265 | ||
|
|
fcd9c19f0b | ||
|
|
430823dde0 | ||
|
|
7e919a7221 | ||
|
|
7e7f055c34 | ||
|
|
6e04ed7c83 | ||
|
|
5ec07f9620 | ||
|
|
d3da4a8a1e | ||
|
|
9ee10dc5f8 | ||
|
|
572963d987 | ||
|
|
636be8989e | ||
|
|
dea7fd71ef | ||
|
|
be08ed8551 | ||
|
|
65ce588287 | ||
|
|
0cc367b25e | ||
|
|
2ce8549502 | ||
|
|
7245d4eab2 | ||
|
|
260607cb72 | ||
|
|
810738539b | ||
|
|
80539949fb | ||
|
|
ebe5c5b113 | ||
|
|
525182c2a7 | ||
|
|
3704c0cb12 | ||
|
|
c8c844cfba | ||
|
|
4c4e16b0c9 | ||
|
|
69f727c4e5 | ||
|
|
04cff4acb1 | ||
|
|
4e4fcc3ae4 | ||
|
|
485f9d6025 | ||
|
|
a697ade93a | ||
|
|
7db07b5a94 | ||
|
|
9ec50875a2 | ||
|
|
02b6875eec | ||
|
|
e7e4c534bc | ||
|
|
e438fa1d99 | ||
|
|
4abbf50a46 | ||
|
|
3e9279d89a | ||
|
|
1305277c09 | ||
|
|
3c47c672d4 | ||
|
|
258d99cd41 | ||
|
|
83356fa4ef | ||
|
|
25429e78f8 | ||
|
|
de90bd1bb0 | ||
|
|
aa6cb4c1d2 | ||
|
|
e871ba600f | ||
|
|
fe3698980d | ||
|
|
89b640f71c | ||
|
|
d13a9cd04a | ||
|
|
c0dab92d0e | ||
|
|
9c897972ad | ||
|
|
307e244475 | ||
|
|
9d3aca646b | ||
|
|
e6e61466df | ||
|
|
db7f9fe2ab | ||
|
|
ded798fdf1 | ||
|
|
bfe94e3068 | ||
|
|
823f07409a | ||
|
|
1a4bc720c2 | ||
|
|
73cacdec24 | ||
|
|
18998c4dbe | ||
|
|
e236d6e912 | ||
|
|
523df670df | ||
|
|
ce58181fc3 | ||
|
|
bed0669f73 | ||
|
|
b0d077e104 | ||
|
|
bdb2951330 | ||
|
|
87a01208fb | ||
|
|
2daee84fbf | ||
|
|
cdbf58f3ac | ||
|
|
f6a59bdf55 | ||
|
|
e12edd977e | ||
|
|
ea72d44b42 | ||
|
|
b925c991eb | ||
|
|
1e7f43fe3d | ||
|
|
d1b9b5546b | ||
|
|
6f21de1901 | ||
|
|
25e8eaa1d4 | ||
|
|
84f0affaed | ||
|
|
6fe736ce06 | ||
|
|
482b03c2d1 | ||
|
|
ce3c72fc47 | ||
|
|
1ff5ed4141 | ||
|
|
fc4b5c6b1d | ||
|
|
10e3421572 | ||
|
|
0117237988 | ||
|
|
2c232a71d5 | ||
|
|
373fd817d0 | ||
|
|
0ef2d9646d | ||
|
|
c8ac417200 | ||
|
|
661bedbe5b | ||
|
|
2dd56590d3 | ||
|
|
98b760251c | ||
|
|
b97de32a44 | ||
|
|
92c8b54f85 | ||
|
|
d8420f472c | ||
|
|
34d93c4de0 | ||
|
|
1c400cd456 | ||
|
|
f8e1758788 | ||
|
|
5c95c72f40 | ||
|
|
98f816b069 | ||
|
|
3ca6308dd2 | ||
|
|
a2c2aa11ac | ||
|
|
a3cf97fcab | ||
|
|
1a9dc7a377 | ||
|
|
806daebd3f | ||
|
|
dfd8845864 | ||
|
|
b5b772d0c2 | ||
|
|
170108b339 | ||
|
|
9f1f58a9c7 | ||
|
|
4d8a2e635c | ||
|
|
0852d55837 | ||
|
|
4c38ddf3cd | ||
|
|
e15edeadb5 | ||
|
|
422c7baada | ||
|
|
4288e2d986 | ||
|
|
4b289388bf | ||
|
|
2a2f8e51b3 | ||
|
|
ed0136090c | ||
|
|
0d70545b98 | ||
|
|
2252091b53 | ||
|
|
85b1875a22 | ||
|
|
386531884b | ||
|
|
afbe328aa7 | ||
|
|
b7c0fba48b | ||
|
|
68635b1629 | ||
|
|
3bd0058dc8 | ||
|
|
270a512585 | ||
|
|
ec2519eae4 | ||
|
|
d7f52d864a | ||
|
|
5cb2b3940e | ||
|
|
b5efaa944d | ||
|
|
bbcfc420d2 | ||
|
|
8ebf7baa71 | ||
|
|
742d2b5ff2 | ||
|
|
547b58f05a | ||
|
|
432aa9f1e1 | ||
|
|
c5bed4f36d | ||
|
|
334306e3c9 | ||
|
|
2a7606836c | ||
|
|
79c966f9e4 | ||
|
|
65f960db00 | ||
|
|
8670f386dc | ||
|
|
54e673176c | ||
|
|
189c60a6d1 | ||
|
|
a718f2a012 |
@@ -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
|
||||
|
||||
16
.prettierrc.js
Normal file
16
.prettierrc.js
Normal file
@@ -0,0 +1,16 @@
|
||||
exports.default = {
|
||||
printWidth: 120,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
trailingComma: "es5",
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
bracketSpacing: true,
|
||||
arrowParens: "always",
|
||||
jsxSingleQuote: false,
|
||||
bracketSameLine: false,
|
||||
endOfLine: "lf",
|
||||
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
||||
importOrderSeparation: true,
|
||||
importOrderSortSpecifiers: true,
|
||||
};
|
||||
184
_reference/reportFiltersAndSorters.md
Normal file
184
_reference/reportFiltersAndSorters.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# 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.
|
||||
|
||||
For filters and sorters, valid types include (`type` key in the schema):
|
||||
- string (default)
|
||||
- number
|
||||
- bool or boolean
|
||||
- date
|
||||
|
||||
## Special Notes
|
||||
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
|
||||
|
||||
## 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.
|
||||
|
||||
## Reflection
|
||||
|
||||
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "jobs.status",
|
||||
"translation": "jobs.fields.status",
|
||||
"label": "Status",
|
||||
"type": "string",
|
||||
"reflector": {
|
||||
"type": "internal",
|
||||
"name": "special.job_statuses"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses`
|
||||
|
||||
The following cases are available
|
||||
|
||||
- `special.job_statuses` - This will reflect the statuses of the jobs table `bodyshop.md_ro_statuses.statuses'`
|
||||
- `special.cost_centers` - This will reflect the cost centers `bodyshop.md_responsibility_centers.costs`
|
||||
- `special.categories` - This will reflect the categories `bodyshop.md_categories`
|
||||
- `special.insurance_companies` - This will reflect the insurance companies `bodyshop.md_ins_cos`'
|
||||
- `special.employee_teams` - This will reflect the employee teams `bodyshop.employee_teams`
|
||||
- `special.employees` - This will reflect the employees `bodyshop.employees`
|
||||
- `special.first_names` - This will reflect the first names `bodyshop.employees`
|
||||
- `special.last_names` - This will reflect the last names `bodyshop.employees`
|
||||
- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources
|
||||
- `special.class`- This will reflect the class `bodyshop.md_classes`
|
||||
-
|
||||
### 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 type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' 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.
|
||||
|
||||
### Default Sorters
|
||||
|
||||
- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
|
||||
- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "jobs.joblines.mod_lb_hrs",
|
||||
"translation": "jobs.joblines.mod_lb_hrs_1",
|
||||
"label": "mod_lb_hrs_1",
|
||||
"type": "number",
|
||||
"default": {
|
||||
"order": 1,
|
||||
"direction": "asc"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
3
client/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
@@ -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: {
|
||||
|
||||
649
client/package-lock.json
generated
649
client/package-lock.json
generated
@@ -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",
|
||||
@@ -22110,19 +22352,6 @@
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-compare": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
|
||||
@@ -23080,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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "../redux/user/user.selectors";
|
||||
import PrivateRoute from "../utils/private-route";
|
||||
import "./App.styles.scss";
|
||||
import handleBeta from "../utils/handleBeta";
|
||||
|
||||
const ResetPassword = lazy(() =>
|
||||
import("../pages/reset-password/reset-password.component")
|
||||
@@ -57,7 +58,6 @@ export function App({
|
||||
if (!navigator.onLine) {
|
||||
setOnline(false);
|
||||
}
|
||||
|
||||
checkUserSession();
|
||||
}, [checkUserSession, setOnline]);
|
||||
|
||||
@@ -73,6 +73,7 @@ export function App({
|
||||
window.addEventListener("online", function (e) {
|
||||
setOnline(true);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser.authorized && bodyshop) {
|
||||
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
||||
@@ -107,6 +108,8 @@ export function App({
|
||||
/>
|
||||
);
|
||||
|
||||
handleBeta();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -210,7 +211,7 @@ export function AccountingPayablesTableComponent({
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top", pageSize: 50 }}
|
||||
pagination={{ position: "top", pageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -15,6 +15,7 @@ import PaymentExportButton from "../payment-export-button/payment-export-button.
|
||||
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
||||
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -209,7 +210,7 @@ export function AccountingPayablesTableComponent({
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={dataSource}
|
||||
pagination={{ position: "top", pageSize: 50 }}
|
||||
pagination={{ position: "top", pageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { alphaSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
export default function AuditTrailListComponent({ loading, data }) {
|
||||
const [state, setState] = useState({
|
||||
@@ -74,7 +75,7 @@ export default function AuditTrailListComponent({ loading, data }) {
|
||||
<Table
|
||||
{...formItemLayout}
|
||||
loading={loading}
|
||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
export default function EmailAuditTrailListComponent({ loading, data }) {
|
||||
const [state, setState] = useState({
|
||||
@@ -53,7 +54,7 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
|
||||
<Table
|
||||
{...formItemLayout}
|
||||
loading={loading}
|
||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
|
||||
@@ -3,10 +3,22 @@ import { useMutation } from "@apollo/client";
|
||||
import { Button, notification, Popconfirm } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { DELETE_BILL } from "../../graphql/bills.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
export default function BillDeleteButton({ bill, callback }) {
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
|
||||
|
||||
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [deleteBill] = useMutation(DELETE_BILL);
|
||||
@@ -36,6 +48,10 @@ export default function BillDeleteButton({ bill, callback }) {
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("bills.successes.deleted") });
|
||||
insertAuditTrail({
|
||||
jobid: jobid,
|
||||
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
|
||||
});
|
||||
|
||||
if (callback && typeof callback === "function") callback(bill.id);
|
||||
} else {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
DELETE_BILL_LINE,
|
||||
INSERT_NEW_BILL_LINES,
|
||||
UPDATE_BILL_LINE
|
||||
UPDATE_BILL_LINE,
|
||||
} from "../../graphql/bill-lines.queries";
|
||||
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
@@ -20,6 +20,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
||||
import BillPrintButton from "../bill-print-button/bill-print-button.component";
|
||||
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
|
||||
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
||||
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||
@@ -176,7 +177,7 @@ export function BillDetailEditcontainer({
|
||||
extra={
|
||||
<Space>
|
||||
<BillDetailEditReturn data={data} />
|
||||
|
||||
<BillPrintButton billid={search.billid} />
|
||||
<Popconfirm
|
||||
visible={visible}
|
||||
onConfirm={() => form.submit()}
|
||||
|
||||
@@ -94,6 +94,7 @@ function BillEnterModalContainer({
|
||||
location,
|
||||
outstanding_returns,
|
||||
inventory,
|
||||
federal_tax_exempt,
|
||||
...remainingValues
|
||||
} = values;
|
||||
|
||||
|
||||
@@ -79,6 +79,20 @@ export function BillFormComponent({
|
||||
});
|
||||
};
|
||||
|
||||
const handleFederalTaxExemptSwitchToggle = (checked) => {
|
||||
// Early gate
|
||||
if (!checked) return;
|
||||
const values = form.getFieldsValue("billlines");
|
||||
// Gate bill lines
|
||||
if (!values?.billlines?.length) return;
|
||||
|
||||
const billlines = values.billlines.map((b) => {
|
||||
b.applicable_taxes.federal = false;
|
||||
return b;
|
||||
});
|
||||
form.setFieldsValue({ billlines });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (job) form.validateFields(["is_credit_memo"]);
|
||||
}, [job, form]);
|
||||
@@ -387,7 +401,16 @@ export function BillFormComponent({
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate span={15}>
|
||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||
<Form.Item
|
||||
span={2}
|
||||
label={t("bills.labels.federal_tax_exempt")}
|
||||
name="federal_tax_exempt"
|
||||
>
|
||||
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item shouldUpdate span={13}>
|
||||
{() => {
|
||||
const values = form.getFieldsValue([
|
||||
"billlines",
|
||||
@@ -405,7 +428,7 @@ export function BillFormComponent({
|
||||
totals = CalculateBillTotal(values);
|
||||
if (!!totals)
|
||||
return (
|
||||
<div>
|
||||
<div align="right">
|
||||
<Space wrap>
|
||||
<Statistic
|
||||
title={t("bills.labels.subtotal")}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import {
|
||||
Button, Form,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -466,7 +467,8 @@ export function BillEnterModalLinesComponent({
|
||||
return {
|
||||
key: `${field.index}fedtax`,
|
||||
valuePropName: "checked",
|
||||
initialValue: true,
|
||||
initialValue:
|
||||
form.getFieldValue("federal_tax_exempt") === true ? false : true,
|
||||
name: [field.name, "applicable_taxes", "federal"],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Button, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
|
||||
export default function BillPrintButton({ billid }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const Templates = TemplateList("job_special");
|
||||
|
||||
const submitHandler = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await GenerateDocument(
|
||||
{
|
||||
name: Templates.parts_invoice_label_single.key,
|
||||
variables: {
|
||||
id: billid,
|
||||
},
|
||||
},
|
||||
{},
|
||||
"p"
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Warning: Error generating a document.");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
<Button loading={loading} onClick={submitHandler}>
|
||||
{t("bills.labels.printlabels")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
@@ -58,7 +58,7 @@ export function BillsListTableComponent({
|
||||
<EditFilled />
|
||||
</Button>
|
||||
)}
|
||||
<BillDeleteButton bill={record} />
|
||||
<BillDeleteButton bill={record} jobid={job.id} />
|
||||
<BillDetailEditReturnComponent
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
||||
disabled={
|
||||
|
||||
@@ -4,6 +4,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
|
||||
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
||||
import ChatLabelComponent from "../chat-label/chat-label.component";
|
||||
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||
|
||||
export default function ChatConversationTitle({ conversation }) {
|
||||
@@ -13,6 +14,7 @@ export default function ChatConversationTitle({ conversation }) {
|
||||
{conversation && conversation.phone_num}
|
||||
</PhoneNumberFormatter>
|
||||
<ChatLabelComponent conversation={conversation} />
|
||||
<ChatPrintButton conversation={conversation} />
|
||||
<ChatConversationTitleTags
|
||||
jobConversations={
|
||||
(conversation && conversation.job_conversations) || []
|
||||
|
||||
@@ -132,7 +132,7 @@ export function ChatPopupComponent({
|
||||
onClick={() => toggleChatVisible()}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<MessageOutlined />
|
||||
<MessageOutlined className="chat-popup-info-icon" />
|
||||
<strong>{t("messaging.labels.messaging")}</strong>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.chat-popup-info-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
.chat-popup {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
|
||||
import { Space, Spin } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
});
|
||||
|
||||
export function ChatPrintButton({ conversation }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
<PrinterOutlined
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
GenerateDocument(
|
||||
{
|
||||
name: TemplateList("messaging").conversation_list.key,
|
||||
variables: { id: conversation.id },
|
||||
},
|
||||
{
|
||||
subject: TemplateList("messaging").conversation_list.subject,
|
||||
},
|
||||
"p",
|
||||
conversation.id
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
/>
|
||||
<MailOutlined
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
GenerateDocument(
|
||||
{
|
||||
name: TemplateList("messaging").conversation_list.key,
|
||||
variables: { id: conversation.id },
|
||||
},
|
||||
{
|
||||
subject: TemplateList("messaging").conversation_list.subject,
|
||||
},
|
||||
"e",
|
||||
conversation.id
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
/>
|
||||
{loading && <Spin />}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);
|
||||
@@ -35,6 +35,15 @@ export default function ContractsCarsComponent({
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => <div>{t(record.status)}</div>,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.readiness"),
|
||||
dataIndex: "readiness",
|
||||
key: "readiness",
|
||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.readiness),
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
dataIndex: "year",
|
||||
|
||||
@@ -68,6 +68,30 @@ export default function ContractFormComponent({
|
||||
<FormDateTimePicker />
|
||||
</Form.Item>
|
||||
)}
|
||||
{create && (
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
|
||||
>
|
||||
{() => {
|
||||
const insuranceOver =
|
||||
selectedCar &&
|
||||
selectedCar.insuranceexpires &&
|
||||
moment(selectedCar.insuranceexpires)
|
||||
.endOf("day")
|
||||
.isBefore(moment(form.getFieldValue("scheduledreturn")));
|
||||
if (insuranceOver)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.insuranceexpired")}
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
@@ -90,16 +114,17 @@ export default function ContractFormComponent({
|
||||
>
|
||||
{() => {
|
||||
const mileageOver =
|
||||
selectedCar &&
|
||||
selectedCar.nextservicekm <= form.getFieldValue("kmstart");
|
||||
|
||||
selectedCar && selectedCar.nextservicekm
|
||||
? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
|
||||
: false;
|
||||
const dueForService =
|
||||
selectedCar &&
|
||||
selectedCar.nextservicedate &&
|
||||
moment(selectedCar.nextservicedate).isBefore(
|
||||
moment(form.getFieldValue("scheduledreturn"))
|
||||
);
|
||||
|
||||
moment(selectedCar.nextservicedate)
|
||||
.endOf("day")
|
||||
.isSameOrBefore(
|
||||
moment(form.getFieldValue("scheduledreturn"))
|
||||
);
|
||||
if (mileageOver || dueForService)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
@@ -117,7 +142,6 @@ export default function ContractFormComponent({
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
export default function ContractsJobsComponent({
|
||||
loading,
|
||||
@@ -175,7 +176,7 @@ export default function ContractsJobsComponent({
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
defaultPageSize: 10,
|
||||
defaultPageSize: pageLimit,
|
||||
defaultCurrent: defaultCurrent,
|
||||
}}
|
||||
columns={columns}
|
||||
|
||||
@@ -13,6 +13,7 @@ import moment from "moment";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -209,7 +210,7 @@ export function ContractsList({
|
||||
}}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 25,
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
export default function CourtesyCarContractListComponent({
|
||||
contracts,
|
||||
@@ -89,7 +90,7 @@ export default function CourtesyCarContractListComponent({
|
||||
scroll={{ x: true }}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 25,
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: totalContracts,
|
||||
}}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
|
||||
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
//import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
@@ -213,6 +214,9 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||
>
|
||||
<CourtesyCarStatus />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("courtesycars.fields.readiness")} name="readiness">
|
||||
<CourtesyCarReadiness />
|
||||
</Form.Item>
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.nextservicekm")}
|
||||
@@ -227,9 +231,9 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||
>
|
||||
{() => {
|
||||
const nextservicekm = form.getFieldValue("nextservicekm");
|
||||
const mileageOver =
|
||||
nextservicekm <= form.getFieldValue("mileage");
|
||||
|
||||
const mileageOver = nextservicekm
|
||||
? nextservicekm <= form.getFieldValue("mileage")
|
||||
: false;
|
||||
if (mileageOver)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
|
||||
@@ -34,6 +34,32 @@ const CourtesyCarFuelComponent = (props, ref) => {
|
||||
step={null}
|
||||
style={{ marginLeft: "2rem", marginRight: "2rem" }}
|
||||
{...props}
|
||||
tooltip={{
|
||||
formatter: (value) => {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return t("courtesycars.labels.fuel.empty");
|
||||
case 13:
|
||||
return t("courtesycars.labels.fuel.18");
|
||||
case 25:
|
||||
return t("courtesycars.labels.fuel.14");
|
||||
case 38:
|
||||
return t("courtesycars.labels.fuel.38");
|
||||
case 50:
|
||||
return t("courtesycars.labels.fuel.12");
|
||||
case 63:
|
||||
return t("courtesycars.labels.fuel.58");
|
||||
case 75:
|
||||
return t("courtesycars.labels.fuel.34");
|
||||
case 88:
|
||||
return t("courtesycars.labels.fuel.78");
|
||||
case 100:
|
||||
return t("courtesycars.labels.fuel.full");
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Select } from "antd";
|
||||
import React, { forwardRef, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const { Option } = Select;
|
||||
|
||||
const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => {
|
||||
const [option, setOption] = useState(value);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== option && onChange) {
|
||||
onChange(option);
|
||||
}
|
||||
}, [value, option, onChange]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
allowClear
|
||||
ref={ref}
|
||||
value={option}
|
||||
style={{
|
||||
width: 100,
|
||||
}}
|
||||
onChange={setOption}
|
||||
>
|
||||
<Option value="courtesycars.readiness.ready">
|
||||
{t("courtesycars.readiness.ready")}
|
||||
</Option>
|
||||
<Option value="courtesycars.readiness.notready">
|
||||
{t("courtesycars.readiness.notready")}
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default forwardRef(CourtesyCarReadinessComponent);
|
||||
@@ -17,13 +17,18 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
});
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [filter, setFilter] = useLocalStorage(
|
||||
"filter_courtesy_cars_list",
|
||||
null
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
@@ -50,6 +55,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
filteredValue: filter?.status || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.status.in"),
|
||||
@@ -72,18 +78,34 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
const { nextservicedate, nextservicekm, mileage } = record;
|
||||
const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
|
||||
record;
|
||||
|
||||
const mileageOver = nextservicekm <= mileage;
|
||||
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
|
||||
|
||||
const dueForService =
|
||||
nextservicedate && moment(nextservicedate).isBefore(moment());
|
||||
nextservicedate &&
|
||||
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
|
||||
|
||||
const insuranceOver =
|
||||
insuranceexpires &&
|
||||
moment(insuranceexpires).endOf("day").isBefore(moment());
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{t(record.status)}
|
||||
{(mileageOver || dueForService) && (
|
||||
<Tooltip title={t("contracts.labels.cardueforservice")}>
|
||||
{(mileageOver || dueForService || insuranceOver) && (
|
||||
<Tooltip
|
||||
title={
|
||||
(mileageOver || dueForService) && insuranceOver
|
||||
? t("contracts.labels.insuranceexpired") +
|
||||
" / " +
|
||||
t("contracts.labels.cardueforservice")
|
||||
: insuranceOver
|
||||
? t("contracts.labels.insuranceexpired")
|
||||
: t("contracts.labels.cardueforservice")
|
||||
}
|
||||
>
|
||||
<WarningFilled style={{ color: "tomato" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -91,6 +113,27 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.readiness"),
|
||||
dataIndex: "readiness",
|
||||
key: "readiness",
|
||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||
filteredValue: filter?.readiness || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.readiness.ready"),
|
||||
value: "courtesycars.readiness.ready",
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.readiness.notready"),
|
||||
value: "courtesycars.readiness.notready",
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => value.includes(record.readiness),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
|
||||
render: (text, record) => t(record.readiness),
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.year"),
|
||||
dataIndex: "year",
|
||||
@@ -131,6 +174,36 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.fields.fuel"),
|
||||
dataIndex: "fuel",
|
||||
key: "fuel",
|
||||
sorter: (a, b) => alphaSort(a.fuel, b.fuel),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
switch (record.fuel) {
|
||||
case 100:
|
||||
return t("courtesycars.labels.fuel.full");
|
||||
case 88:
|
||||
return t("courtesycars.labels.fuel.78");
|
||||
case 63:
|
||||
return t("courtesycars.labels.fuel.58");
|
||||
case 50:
|
||||
return t("courtesycars.labels.fuel.12");
|
||||
case 38:
|
||||
return t("courtesycars.labels.fuel.34");
|
||||
case 25:
|
||||
return t("courtesycars.labels.fuel.14");
|
||||
case 13:
|
||||
return t("courtesycars.labels.fuel.18");
|
||||
case 0:
|
||||
return t("courtesycars.labels.fuel.empty");
|
||||
default:
|
||||
return record.fuel;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("courtesycars.labels.outwith"),
|
||||
dataIndex: "outwith",
|
||||
@@ -161,7 +234,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
setFilter(filters);
|
||||
};
|
||||
|
||||
const tableData = searchText
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -5,8 +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 { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function CsiResponseListPaginated({
|
||||
refetch,
|
||||
@@ -15,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")}
|
||||
@@ -40,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>
|
||||
) : (
|
||||
@@ -63,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>
|
||||
@@ -75,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) => {
|
||||
@@ -106,8 +113,8 @@ export default function CsiResponseListPaginated({
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 25,
|
||||
current: parseInt(page || 1),
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(state.page || 1),
|
||||
total: total,
|
||||
}}
|
||||
columns={columns}
|
||||
@@ -121,13 +128,6 @@ export default function CsiResponseListPaginated({
|
||||
selectedRowKeys: [responseid],
|
||||
type: "radio",
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleOnRowClick(record);
|
||||
}, // click row
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { alphaSort } from "../../../utils/sorters";
|
||||
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
||||
import Dinero from "dinero.js";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
import {pageLimit} from "../../../utils/config";
|
||||
|
||||
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -118,7 +119,7 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
scroll={{ x: true, y: "calc(100% - 4em)" }}
|
||||
rowKey="id"
|
||||
|
||||
@@ -3,20 +3,32 @@ import {
|
||||
ExclamationCircleFilled,
|
||||
PauseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Space, Table, Tooltip } from "antd";
|
||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../../owner-name-display/owner-name-display.component";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {},
|
||||
});
|
||||
const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage(
|
||||
"isTvModeScheduledIn",
|
||||
false
|
||||
);
|
||||
|
||||
if (!data) return null;
|
||||
if (!data.scheduled_in_today)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
@@ -30,6 +42,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
alt_transport: item.job.alt_transport,
|
||||
clm_no: item.job.clm_no,
|
||||
jobid: item.job.jobid,
|
||||
joblines_body: item.job.joblines
|
||||
.filter((l) => l.mod_lbr_ty !== "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
|
||||
joblines_ref: item.job.joblines
|
||||
.filter((l) => l.mod_lbr_ty === "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
|
||||
ins_co_nm: item.job.ins_co_nm,
|
||||
iouparent: item.job.iouparent,
|
||||
ownerid: item.job.ownerid,
|
||||
@@ -48,7 +66,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
v_vin: item.job.v_vin,
|
||||
vehicleid: item.job.vehicleid,
|
||||
note: item.note,
|
||||
start: moment(item.start).format("hh:mm a"),
|
||||
start: item.start,
|
||||
title: item.title,
|
||||
};
|
||||
appt.push(i);
|
||||
@@ -58,11 +76,192 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
return new moment(a.start) - new moment(b.start);
|
||||
});
|
||||
|
||||
const columns = [
|
||||
const tvFontSize = 16;
|
||||
const tvFontWeight = "bold";
|
||||
|
||||
const tvColumns = [
|
||||
{
|
||||
title: t("appointments.fields.time"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.start, b.start),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<TimeFormatter>{record.start}</TimeFormatter>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Space>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{record.suspended && (
|
||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
||||
)}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
to={"/manage/owners/" + record.ownerid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
to={"/manage/vehicles/" + record.vehicleid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
|
||||
record.v_model_yr || ""
|
||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(appt &&
|
||||
appt
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.alt_transport}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lab"),
|
||||
dataIndex: "joblines_body",
|
||||
key: "joblines_body",
|
||||
sorter: (a, b) => a.joblines_body - b.joblines_body,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "joblines_body" &&
|
||||
state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.joblines_body.toFixed(1)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lar"),
|
||||
dataIndex: "joblines_ref",
|
||||
key: "joblines_ref",
|
||||
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.joblines_ref.toFixed(1)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("appointments.fields.time"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.start, b.start),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => <TimeFormatter>{record.start}</TimeFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
@@ -90,7 +289,10 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
@@ -107,23 +309,16 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
title: t("dashboard.labels.phone"),
|
||||
dataIndex: "ownr_ph",
|
||||
key: "ownr_ph",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
<Space size="small" wrap>
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -133,7 +328,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
|
||||
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -141,6 +336,15 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
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:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
@@ -164,43 +368,80 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.time"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
|
||||
filters:
|
||||
(appt &&
|
||||
appt
|
||||
.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("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(appt &&
|
||||
appt
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (sorter) => {
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledintoday", {
|
||||
title={t("dashboard.titles.scheduledindate", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
extra={
|
||||
<Space>
|
||||
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
|
||||
<Switch
|
||||
onClick={() => setIsTvModeScheduledIn(!isTvModeScheduledIn)}
|
||||
defaultChecked={isTvModeScheduledIn}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
columns={isTvModeScheduledIn ? tvColumns : columns}
|
||||
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
||||
rowKey="id"
|
||||
style={{ height: "85%" }}
|
||||
dataSource={appt}
|
||||
size={isTvModeScheduledIn ? "small" : "middle"}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -219,6 +460,10 @@ export const DashboardScheduledInTodayGql = `
|
||||
alt_transport
|
||||
clm_no
|
||||
jobid: id
|
||||
joblines(where: {removed: {_eq: false}}) {
|
||||
mod_lb_hrs
|
||||
mod_lbr_ty
|
||||
}
|
||||
ins_co_nm
|
||||
iouparent
|
||||
ownerid
|
||||
|
||||
@@ -3,36 +3,272 @@ import {
|
||||
ExclamationCircleFilled,
|
||||
PauseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Space, Table, Tooltip } from "antd";
|
||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../../owner-name-display/owner-name-display.component";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {},
|
||||
});
|
||||
const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage(
|
||||
"isTvModeScheduledOut",
|
||||
false
|
||||
);
|
||||
|
||||
if (!data) return null;
|
||||
if (!data.scheduled_out_today)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
data.scheduled_out_today.forEach((item) => {
|
||||
item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
|
||||
item.joblines_body = item.joblines
|
||||
? item.joblines
|
||||
.filter((l) => l.mod_lbr_ty !== "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||
: 0;
|
||||
item.joblines_ref = item.joblines
|
||||
? item.joblines
|
||||
.filter((l) => l.mod_lbr_ty === "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||
: 0;
|
||||
});
|
||||
data.scheduled_out_today.sort(function (a, b) {
|
||||
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
|
||||
});
|
||||
|
||||
const columns = [
|
||||
const tvFontSize = 18;
|
||||
const tvFontWeight = "bold";
|
||||
|
||||
const tvColumns = [
|
||||
{
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
dateSort(a.scheduled_completion, b.scheduled_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "scheduled_completion" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Space>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{record.suspended && (
|
||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
||||
)}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
to={"/manage/owners/" + record.ownerid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
to={"/manage/vehicles/" + record.vehicleid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
|
||||
record.v_model_yr || ""
|
||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(data.scheduled_out_today &&
|
||||
data.scheduled_out_today
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.alt_transport}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filters:
|
||||
(data.scheduled_out_today &&
|
||||
data.scheduled_out_today
|
||||
.map((j) => j.status)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Status*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.status}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lab"),
|
||||
dataIndex: "joblines_body",
|
||||
key: "joblines_body",
|
||||
sorter: (a, b) => a.joblines_body - b.joblines_body,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "joblines_body" &&
|
||||
state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.joblines_body.toFixed(1)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lar"),
|
||||
dataIndex: "joblines_ref",
|
||||
key: "joblines_ref",
|
||||
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.joblines_ref.toFixed(1)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
dateSort(a.scheduled_completion, b.scheduled_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "scheduled_completion" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
@@ -60,7 +296,10 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
@@ -77,23 +316,16 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
title: t("dashboard.labels.phone"),
|
||||
dataIndex: "ownr_ph",
|
||||
key: "ownr_ph",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
<Space size="small" wrap>
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -103,7 +335,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
|
||||
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -111,6 +343,15 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
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:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
@@ -134,43 +375,80 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
|
||||
filters:
|
||||
(data.scheduled_out_today &&
|
||||
data.scheduled_out_today
|
||||
.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("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(data.scheduled_out_today &&
|
||||
data.scheduled_out_today
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (sorter) => {
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledouttoday", {
|
||||
title={t("dashboard.titles.scheduledoutdate", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
extra={
|
||||
<Space>
|
||||
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
|
||||
<Switch
|
||||
onClick={() => setIsTvModeScheduledOut(!isTvModeScheduledOut)}
|
||||
defaultChecked={isTvModeScheduledOut}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
columns={isTvModeScheduledOut ? tvColumns : columns}
|
||||
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
||||
rowKey="id"
|
||||
style={{ height: "85%" }}
|
||||
dataSource={data.scheduled_out_today}
|
||||
size={isTvModeScheduledOut ? "small" : "middle"}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -187,6 +465,10 @@ export const DashboardScheduledOutTodayGql = `
|
||||
alt_transport
|
||||
clm_no
|
||||
jobid: id
|
||||
joblines(where: {removed: {_eq: false}}) {
|
||||
mod_lb_hrs
|
||||
mod_lbr_ty
|
||||
}
|
||||
ins_co_nm
|
||||
iouparent
|
||||
ownerid
|
||||
@@ -199,6 +481,7 @@ export const DashboardScheduledOutTodayGql = `
|
||||
production_vars
|
||||
ro_number
|
||||
scheduled_completion
|
||||
status
|
||||
suspended
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
|
||||
@@ -275,26 +275,22 @@ const componentList = {
|
||||
h: 2,
|
||||
},
|
||||
ScheduleInToday: {
|
||||
label: i18next.t("dashboard.titles.scheduledintoday", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
}),
|
||||
label: i18next.t("dashboard.titles.scheduledintoday"),
|
||||
component: DashboardScheduledInToday,
|
||||
gqlFragment: DashboardScheduledInTodayGql,
|
||||
minW: 10,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
w: 10,
|
||||
h: 2,
|
||||
h: 3,
|
||||
},
|
||||
ScheduleOutToday: {
|
||||
label: i18next.t("dashboard.titles.scheduledouttoday", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
}),
|
||||
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
||||
component: DashboardScheduledOutToday,
|
||||
gqlFragment: DashboardScheduledOutTodayGql,
|
||||
minW: 10,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
w: 10,
|
||||
h: 2,
|
||||
h: 3,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -306,8 +302,7 @@ const createDashboardQuery = (state) => {
|
||||
.map((item, index) => componentList[item.i].gqlFragment || "")
|
||||
.join("");
|
||||
return gql`
|
||||
query QUERY_DASHBOARD_DETAILS {
|
||||
${componentBasedAdditions || ""}
|
||||
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
||||
monthly_sales: jobs(where: {_and: [
|
||||
{ voided: {_eq: false}},
|
||||
{date_invoiced: {_gte: "${moment()
|
||||
@@ -317,11 +312,11 @@ const createDashboardQuery = (state) => {
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}]}) {
|
||||
id
|
||||
ro_number
|
||||
date_invoiced
|
||||
job_totals
|
||||
rate_la1
|
||||
id
|
||||
ro_number
|
||||
date_invoiced
|
||||
job_totals
|
||||
rate_la1
|
||||
rate_la2
|
||||
rate_la3
|
||||
rate_la4
|
||||
@@ -344,43 +339,42 @@ const createDashboardQuery = (state) => {
|
||||
rate_mapa
|
||||
rate_mash
|
||||
rate_matd
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
}
|
||||
}
|
||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
ro_number
|
||||
ins_co_nm
|
||||
job_totals
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
ro_number
|
||||
ins_co_nm
|
||||
job_totals
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}`;
|
||||
};
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.ant-card-body {
|
||||
height: 80%;
|
||||
height: calc(100% - 2rem);
|
||||
width: 100%;
|
||||
// // background-color: red;
|
||||
// height: 90%;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -117,7 +118,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
|
||||
}
|
||||
>
|
||||
<Table
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey={(record) => `${record.InvoiceNumber}${record.Account}`}
|
||||
dataSource={allocationsSummary}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import Dinero from "dinero.js";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -94,7 +95,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
<Alert type="warning" message={t("jobs.labels.dms.disablebillwip")} />
|
||||
)}
|
||||
<Table
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="center"
|
||||
dataSource={allocationsSummary}
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
|
||||
@@ -65,8 +65,17 @@ export function FormDatePicker({
|
||||
});
|
||||
}
|
||||
|
||||
if (_a.isValid() && onChange)
|
||||
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
|
||||
if (_a.isValid() && onChange) {
|
||||
if (onlyFuture) {
|
||||
if (moment().subtract(1, "day").isBefore(_a)) {
|
||||
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
|
||||
} else {
|
||||
onChange(isDateOnly ? moment().format("YYYY-MM-DD") : moment());
|
||||
}
|
||||
} else {
|
||||
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { forwardRef } from "react";
|
||||
//import DatePicker from "react-datepicker";
|
||||
//import "react-datepicker/src/stylesheets/datepicker.scss";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
import { TimePicker } from "antd";
|
||||
import moment from "moment";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
//To be used as a form element only.
|
||||
|
||||
const DateTimePicker = (
|
||||
@@ -26,20 +26,21 @@ const DateTimePicker = (
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onlyFuture={onlyFuture}
|
||||
isDateOnly={false}
|
||||
/>
|
||||
|
||||
<TimePicker
|
||||
value={value ? moment(value) : null}
|
||||
{...(onlyFuture && {
|
||||
disabledDate: (d) => moment().isAfter(d),
|
||||
})}
|
||||
onChange={onChange}
|
||||
showSecond={false}
|
||||
minuteStep={15}
|
||||
onBlur={onBlur}
|
||||
format="hh:mm a"
|
||||
{...restProps}
|
||||
value={value ? moment(value) : null}
|
||||
{...(onlyFuture && {
|
||||
disabledDate: (d) => moment().isAfter(d),
|
||||
})}
|
||||
onChange={onChange}
|
||||
showSecond={false}
|
||||
minuteStep={15}
|
||||
onBlur={onBlur}
|
||||
format="hh:mm a"
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import Icon, {
|
||||
FileFilled,
|
||||
//GlobalOutlined,
|
||||
HomeFilled,
|
||||
ImportOutlined,
|
||||
ImportOutlined, InfoCircleOutlined,
|
||||
LineChartOutlined,
|
||||
PaperClipOutlined,
|
||||
PhoneOutlined,
|
||||
@@ -26,8 +26,8 @@ import Icon, {
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Layout, Menu } from "antd";
|
||||
import React from "react";
|
||||
import {Layout, Menu, Switch, Tooltip} from "antd";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
import {
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import {handleBeta, setBeta, checkBeta} from "../../utils/handleBeta";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -102,9 +103,21 @@ function Header({
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
const [betaSwitch, setBetaSwitch] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const isBeta = checkBeta();
|
||||
setBetaSwitch(isBeta);
|
||||
}, []);
|
||||
|
||||
const betaSwitchChange = (checked) => {
|
||||
setBeta(checked);
|
||||
setBetaSwitch(checked);
|
||||
handleBeta();
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout.Header>
|
||||
<Menu
|
||||
@@ -431,6 +444,17 @@ function Header({
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
<Menu.Item style={{marginLeft: 'auto'}} key="profile">
|
||||
<Tooltip title="A more modern ImEX Online is ready for you to try! You can switch back at any time.">
|
||||
<InfoCircleOutlined/>
|
||||
<span style={{marginRight: 8}}>Try the new ImEX Online</span>
|
||||
<Switch
|
||||
checked={betaSwitch}
|
||||
onChange={betaSwitchChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Menu.Item>
|
||||
|
||||
</Menu>
|
||||
</Layout.Header>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component";
|
||||
import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -213,7 +214,7 @@ export function JobsList({
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 25,
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
}}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../../redux/application/application.actions";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import InventoryListPaginated from "./inventory-list.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//bodyshop: selectBodyshop,
|
||||
@@ -32,8 +33,8 @@ export function InventoryList({ setBreadcrumbs, setSelectedHeader }) {
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
search: search || "",
|
||||
offset: page ? (page - 1) * 25 : 0,
|
||||
limit: 25,
|
||||
offset: page ? (page - 1) * pageLimit : 0,
|
||||
limit: pageLimit,
|
||||
consumedIsNull: showall === "true" ? null : true,
|
||||
order: [
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import Dinero from "dinero.js";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
export default function JobCostingPartsTable({ data, summaryData }) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [state, setState] = useState({
|
||||
@@ -98,7 +99,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
|
||||
x: "50%", //y: "40rem"
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={filteredData}
|
||||
|
||||
@@ -7,21 +7,31 @@ import { connect } from "react-redux";
|
||||
import { useHistory } from "react-router";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB_LINES_IOU } from "../../graphql/jobs-lines.queries";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { CreateIouForJob } from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
technician: selectTechnician,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU);
|
||||
|
||||
export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) {
|
||||
export function JobCreateIOU({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
job,
|
||||
selectedJobLines,
|
||||
technician,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const client = useApolloClient();
|
||||
@@ -79,13 +89,19 @@ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) {
|
||||
title={t("jobs.labels.createiouwarning")}
|
||||
onConfirm={handleCreateIou}
|
||||
disabled={
|
||||
!selectedJobLines || selectedJobLines.length === 0 || !job.converted
|
||||
!selectedJobLines ||
|
||||
selectedJobLines.length === 0 ||
|
||||
!job.converted ||
|
||||
technician
|
||||
}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={
|
||||
!selectedJobLines || selectedJobLines.length === 0 || !job.converted
|
||||
!selectedJobLines ||
|
||||
selectedJobLines.length === 0 ||
|
||||
!job.converted ||
|
||||
technician
|
||||
}
|
||||
>
|
||||
{t("jobs.actions.createiou")}
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
import React from "react";
|
||||
import { Card } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
|
||||
export default function JobDetailCardTemplate({
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobDetailCardTemplate);
|
||||
|
||||
export function JobDetailCardTemplate({
|
||||
loading,
|
||||
title,
|
||||
extraLink,
|
||||
technician,
|
||||
...otherProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let extra;
|
||||
if (extraLink) extra = { extra: <Link to={extraLink}>More</Link> };
|
||||
if (extraLink && !technician)
|
||||
extra = {
|
||||
extra: <Link to={extraLink}>{t("jobs.labels.cards.more")}</Link>,
|
||||
};
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
|
||||
246
client/src/components/job-lifecycle/job-lifecycle.component.jsx
Normal file
246
client/src/components/job-lifecycle/job-lifecycle.component.jsx
Normal 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;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GET_ALL_JOBLINES_BY_PK } from "../../graphql/jobs-lines.queries";
|
||||
import { gql } from "@apollo/client";
|
||||
import _ from "lodash";
|
||||
import { GET_ALL_JOBLINES_BY_PK } from "../../graphql/jobs-lines.queries";
|
||||
|
||||
export const GetSupplementDelta = async (client, jobId, newLines) => {
|
||||
const {
|
||||
@@ -50,7 +50,7 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
|
||||
.reduce((acc, value, idx) => {
|
||||
return acc + generateRemoveQuery(value, idx);
|
||||
}, "");
|
||||
console.log(insertQueries, updateQueries, removeQueries);
|
||||
//console.log(insertQueries, updateQueries, removeQueries);
|
||||
|
||||
if ((insertQueries + updateQueries + removeQueries).trim() === "") {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -114,7 +114,7 @@ const headerFields = [
|
||||
"ins_ct_ph",
|
||||
"ins_ct_phx",
|
||||
"loss_cat",
|
||||
//ad2
|
||||
//AD2
|
||||
"clmt_ln",
|
||||
"clmt_fn",
|
||||
"clmt_title",
|
||||
@@ -219,7 +219,16 @@ const headerFields = [
|
||||
"loc_title",
|
||||
"loc_ph",
|
||||
"loc_phx",
|
||||
"loc_ea"
|
||||
"loc_ea",
|
||||
//VEH
|
||||
"plate_no",
|
||||
"plate_st",
|
||||
"v_vin",
|
||||
"v_model_yr",
|
||||
"v_make_desc",
|
||||
"v_model_desc",
|
||||
"v_options",
|
||||
"v_color",
|
||||
];
|
||||
|
||||
export default headerFields;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
function updateJobCache(items) {
|
||||
client.cache.modify({
|
||||
id: "ROOT_QUERY",
|
||||
@@ -40,6 +47,7 @@ export function JobsCloseExportButton({
|
||||
disabled,
|
||||
setSelectedJobs,
|
||||
refetch,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
@@ -181,6 +189,10 @@ export function JobsCloseExportButton({
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
|
||||
);
|
||||
@@ -192,12 +204,20 @@ export function JobsCloseExportButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
|
||||
if (
|
||||
bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
successfulTransactions.length > 0
|
||||
) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
});
|
||||
updateJobCache([
|
||||
...new Set(
|
||||
successfulTransactions.map(
|
||||
@@ -227,4 +247,7 @@ export function JobsCloseExportButton({
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(JobsCloseExportButton);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsCloseExportButton);
|
||||
|
||||
@@ -19,10 +19,13 @@ export default function JobsCreateOwnerInfoNewComponent() {
|
||||
label={t("owners.fields.ownr_ln")}
|
||||
name={["owner", "data", "ownr_ln"]}
|
||||
rules={[
|
||||
{
|
||||
required: state.owner.new,
|
||||
({ getFieldValue }) => ({
|
||||
required:
|
||||
state.owner.new &&
|
||||
(!getFieldValue(["owner", "data", "ownr_co_nm"]) ||
|
||||
getFieldValue(["owner", "data", "ownr_co_nm"]) === ""),
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input disabled={!state.owner.new} />
|
||||
@@ -31,10 +34,13 @@ export default function JobsCreateOwnerInfoNewComponent() {
|
||||
label={t("owners.fields.ownr_fn")}
|
||||
name={["owner", "data", "ownr_fn"]}
|
||||
rules={[
|
||||
{
|
||||
required: state.owner.new,
|
||||
({ getFieldValue }) => ({
|
||||
required:
|
||||
state.owner.new &&
|
||||
(!getFieldValue(["owner", "data", "ownr_co_nm"]) ||
|
||||
getFieldValue(["owner", "data", "ownr_co_nm"]) === ""),
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input disabled={!state.owner.new} />
|
||||
@@ -51,6 +57,17 @@ export default function JobsCreateOwnerInfoNewComponent() {
|
||||
<Form.Item
|
||||
label={t("owners.fields.ownr_co_nm")}
|
||||
name={["owner", "data", "ownr_co_nm"]}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
required:
|
||||
state.owner.new &&
|
||||
(!getFieldValue(["owner", "data", "ownr_ln"]) ||
|
||||
!getFieldValue(["owner", "data", "ownr_fn"]) ||
|
||||
getFieldValue(["owner", "data", "ownr_ln"]) === "" ||
|
||||
getFieldValue(["owner", "data", "ownr_fn"]) === ""),
|
||||
//message: t("general.validation.required"),
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input disabled={!state.owner.new} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -108,6 +108,14 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
},
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.alertToggle(
|
||||
!!job.production_vars && !!job.production_vars.alert
|
||||
? !job.production_vars.alert
|
||||
: true
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSuspend = (e) => {
|
||||
@@ -121,6 +129,12 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
},
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobsuspend(
|
||||
!!job.suspended ? !job.suspended : true
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const statusmenu = (
|
||||
@@ -532,6 +546,10 @@ export function JobsDetailHeaderActions({
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.voided"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobvoid(),
|
||||
});
|
||||
//go back to jobs list.
|
||||
history.push(`/manage/`);
|
||||
} else {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
BranchesOutlined,
|
||||
ExclamationCircleFilled,
|
||||
PauseCircleOutlined,
|
||||
WarningFilled,
|
||||
BranchesOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Tag, Tooltip } from "antd";
|
||||
import React, { useState } from "react";
|
||||
@@ -15,6 +15,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
||||
@@ -122,20 +123,23 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
</DataLabel>
|
||||
{job.cccontracts.length > 0 && (
|
||||
<DataLabel label={t("jobs.labels.contracts")}>
|
||||
{job.cccontracts.map((c) => (
|
||||
<Link
|
||||
key={c.id}
|
||||
to={`/manage/courtesycars/contracts/${c.id}`}
|
||||
>{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}</Link>
|
||||
{job.cccontracts.map((c, index) => (
|
||||
<Space wrap>
|
||||
<Link
|
||||
key={c.id}
|
||||
to={`/manage/courtesycars/contracts/${c.id}`}
|
||||
>
|
||||
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
||||
{index !== job.cccontracts.length - 1 ? "," : null}
|
||||
</Link>
|
||||
</Space>
|
||||
))}
|
||||
</DataLabel>
|
||||
)}
|
||||
|
||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||
<ProductionListColumnProductionNote record={job} />
|
||||
</DataLabel>
|
||||
|
||||
<Space>
|
||||
<Space wrap>
|
||||
{job.special_coverage_policy && (
|
||||
<Tag color="tomato">
|
||||
<Space>
|
||||
@@ -160,19 +164,35 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
<Link to={disabled ? "#" : `/manage/owners/${job.owner.id}`}>
|
||||
{ownerTitle.length > 0
|
||||
? ownerTitle
|
||||
: t("owner.labels.noownerinfo")}
|
||||
</Link>
|
||||
disabled ? (
|
||||
<>
|
||||
{ownerTitle.length > 0
|
||||
? ownerTitle
|
||||
: t("owner.labels.noownerinfo")}
|
||||
</>
|
||||
) : (
|
||||
<Link to={`/manage/owners/${job.owner.id}`}>
|
||||
{ownerTitle.length > 0
|
||||
? ownerTitle
|
||||
: t("owner.labels.noownerinfo")}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||
{disabled ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||
{disabled ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel key="3" label={t("owners.fields.address")}>
|
||||
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
|
||||
@@ -180,7 +200,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||
{job.ownr_ea || ""}
|
||||
{disabled ? (
|
||||
<>{job.ownr_ea || ""}</>
|
||||
) : job.ownr_ea ? (
|
||||
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
||||
) : null}
|
||||
</DataLabel>
|
||||
{job.owner?.tax_number && (
|
||||
<DataLabel key="5" label={t("owners.fields.tax_number")}>
|
||||
@@ -195,17 +219,19 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
job.vehicle ? (
|
||||
<Link
|
||||
to={
|
||||
disabled
|
||||
? "#"
|
||||
: job.vehicle && `/manage/vehicles/${job.vehicle.id}`
|
||||
}
|
||||
>
|
||||
{vehicleTitle.length > 0
|
||||
? vehicleTitle
|
||||
: t("vehicles.labels.novehinfo")}
|
||||
</Link>
|
||||
disabled ? (
|
||||
<>
|
||||
{vehicleTitle.length > 0
|
||||
? vehicleTitle
|
||||
: t("vehicles.labels.novehinfo")}{" "}
|
||||
</>
|
||||
) : (
|
||||
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
|
||||
{vehicleTitle.length > 0
|
||||
? vehicleTitle
|
||||
: t("vehicles.labels.novehinfo")}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<span></span>
|
||||
)
|
||||
@@ -221,9 +247,19 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
<VehicleVinDisplay>
|
||||
{`${job.v_vin || t("general.labels.na")}`}
|
||||
</VehicleVinDisplay>
|
||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||
job.v_vin?.length !== 17 ? (
|
||||
<WarningFilled
|
||||
style={{ color: "tomato", marginLeft: ".3rem" }}
|
||||
/>
|
||||
) : null
|
||||
) : null}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.regie_number")}>
|
||||
{job.regie_number || t("general.labels.na")}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||
<JobsRelatedRos jobid={job.id} job={job} />
|
||||
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
||||
</DataLabel>
|
||||
{job.vehicle && job.vehicle.notes && (
|
||||
<DataLabel
|
||||
|
||||
@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_JOBS } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
function updateJobCache(items) {
|
||||
client.cache.modify({
|
||||
id: "ROOT_QUERY",
|
||||
@@ -41,6 +48,7 @@ export function JobsExportAllButton({
|
||||
loadingCallback,
|
||||
completedCallback,
|
||||
refetch,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [updateJob] = useMutation(UPDATE_JOBS);
|
||||
@@ -177,6 +185,12 @@ export function JobsExportAllButton({
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
});
|
||||
});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map(
|
||||
(job) => job.id
|
||||
@@ -190,13 +204,17 @@ export function JobsExportAllButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
|
||||
if (
|
||||
bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
successfulTransactions.length > 0
|
||||
) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
updateJobCache([
|
||||
const successfulTransactionsSet = [
|
||||
...new Set(
|
||||
successfulTransactions.map(
|
||||
(st) =>
|
||||
@@ -207,7 +225,14 @@ export function JobsExportAllButton({
|
||||
]
|
||||
)
|
||||
),
|
||||
]);
|
||||
];
|
||||
if (successfulTransactionsSet.length > 0) {
|
||||
insertAuditTrail({
|
||||
jobid: successfulTransactionsSet[0],
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
});
|
||||
}
|
||||
updateJobCache(successfulTransactionsSet);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -225,4 +250,7 @@ export function JobsExportAllButton({
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(JobsExportAllButton);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsExportAllButton);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,6 +10,8 @@ import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -24,6 +26,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const [openSearchResults, setOpenSearchResults] = useState([]);
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const [filter, setFilter] = useLocalStorage("filter_jobs_all", null);
|
||||
const { page, sortcolumn, sortorder } = search;
|
||||
const history = useHistory();
|
||||
|
||||
@@ -33,17 +36,14 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
|
||||
sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.id}>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "ownr_ln",
|
||||
@@ -95,6 +95,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
render: (text, record) => {
|
||||
return record.status || t("general.labels.na");
|
||||
},
|
||||
filteredValue: filter?.status || null,
|
||||
filters: bodyshop.md_ro_statuses.statuses.map((s) => {
|
||||
return { text: s, value: [s] };
|
||||
}),
|
||||
@@ -125,7 +126,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("vehicles.fields.plate_no"),
|
||||
dataIndex: "plate_no",
|
||||
key: "plate_no",
|
||||
|
||||
ellipsis: true,
|
||||
sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||
sortOrder: sortcolumn === "plate_no" && sortorder,
|
||||
@@ -137,7 +137,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
|
||||
ellipsis: true,
|
||||
sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||
@@ -193,6 +192,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
} else {
|
||||
delete search.statusFilters;
|
||||
}
|
||||
setFilter(filters);
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
};
|
||||
|
||||
@@ -259,11 +259,11 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
pagination={
|
||||
search?.search
|
||||
? {
|
||||
pageSize: 25,
|
||||
pageSize: pageLimit,
|
||||
showSizeChanger: false,
|
||||
}
|
||||
: {
|
||||
pageSize: 25,
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
showSizeChanger: false,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
SyncOutlined,
|
||||
BranchesOutlined,
|
||||
ExclamationCircleFilled,
|
||||
PauseCircleOutlined,
|
||||
BranchesOutlined,
|
||||
SyncOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
|
||||
@@ -14,9 +14,10 @@ import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
@@ -39,10 +40,8 @@ export function JobsList({ bodyshop }) {
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
});
|
||||
const [state, setState] = useState({ sortedInfo: {} });
|
||||
const [filter, setFilter] = useLocalStorage("filter_jobs_list", null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
@@ -91,7 +90,8 @@ export function JobsList({ bodyshop }) {
|
||||
: [];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
setFilter(filters);
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
@@ -117,7 +117,6 @@ export function JobsList({ bodyshop }) {
|
||||
parseInt((b.ro_number || "0").replace(/\D/g, "")),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.id}
|
||||
@@ -145,7 +144,6 @@ export function JobsList({ bodyshop }) {
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sortOrder:
|
||||
@@ -185,16 +183,15 @@ export function JobsList({ bodyshop }) {
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
ellipsis: true,
|
||||
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filteredValue: filter?.status || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
@@ -205,7 +202,14 @@ export function JobsList({ bodyshop }) {
|
||||
text: s || "No Status*",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
})
|
||||
.sort((a, b) =>
|
||||
statusSort(
|
||||
a.text,
|
||||
b.text,
|
||||
bodyshop.md_ro_statuses.active_statuses
|
||||
)
|
||||
)) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
},
|
||||
@@ -262,6 +266,7 @@ export function JobsList({ bodyshop }) {
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
filteredValue: filter?.ins_co_nm || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
@@ -269,10 +274,11 @@ export function JobsList({ bodyshop }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s,
|
||||
text: s || "No Ins. Co.*",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.ins_co_nm),
|
||||
responsive: ["md"],
|
||||
@@ -283,7 +289,6 @@ export function JobsList({ bodyshop }) {
|
||||
key: "clm_total",
|
||||
responsive: ["md"],
|
||||
ellipsis: true,
|
||||
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||
@@ -294,10 +299,11 @@ export function JobsList({ bodyshop }) {
|
||||
{
|
||||
title: t("jobs.labels.estimator"),
|
||||
dataIndex: "jobs.labels.estimator",
|
||||
key: "jobs.labels.estimator",
|
||||
key: "estimator",
|
||||
ellipsis: true,
|
||||
responsive: ["xl"],
|
||||
filterSearch: true,
|
||||
filteredValue: filter?.estimator || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
@@ -305,10 +311,11 @@ export function JobsList({ bodyshop }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "N/A",
|
||||
text: s || "No Estimator*",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) =>
|
||||
value.includes(
|
||||
|
||||
@@ -16,7 +16,9 @@ import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
@@ -52,10 +54,8 @@ export function JobsReadyList({ bodyshop }) {
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
});
|
||||
const [state, setState] = useState({ sortedInfo: {} });
|
||||
const [filter, setFilter] = useLocalStorage("filter_jobs_ready", null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
@@ -104,7 +104,8 @@ export function JobsReadyList({ bodyshop }) {
|
||||
: [];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
setFilter(filters);
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
@@ -128,7 +129,6 @@ export function JobsReadyList({ bodyshop }) {
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.id}
|
||||
@@ -156,7 +156,6 @@ export function JobsReadyList({ bodyshop }) {
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sortOrder:
|
||||
@@ -196,16 +195,15 @@ export function JobsReadyList({ bodyshop }) {
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
ellipsis: true,
|
||||
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filteredValue: filter?.status || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
@@ -216,11 +214,17 @@ export function JobsReadyList({ bodyshop }) {
|
||||
text: s || "No Status*",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
})
|
||||
.sort((a, b) =>
|
||||
statusSort(
|
||||
a.text,
|
||||
b.text,
|
||||
bodyshop.md_ro_statuses.active_statuses
|
||||
)
|
||||
)) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
@@ -273,6 +277,7 @@ export function JobsReadyList({ bodyshop }) {
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
filteredValue: filter?.ins_co_nm || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
@@ -280,10 +285,11 @@ export function JobsReadyList({ bodyshop }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s,
|
||||
text: s || "No Ins Co.*",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.ins_co_nm),
|
||||
responsive: ["md"],
|
||||
@@ -294,7 +300,6 @@ export function JobsReadyList({ bodyshop }) {
|
||||
key: "clm_total",
|
||||
responsive: ["md"],
|
||||
ellipsis: true,
|
||||
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||
@@ -305,9 +310,10 @@ export function JobsReadyList({ bodyshop }) {
|
||||
{
|
||||
title: t("jobs.labels.estimator"),
|
||||
dataIndex: "jobs.labels.estimator",
|
||||
key: "jobs.labels.estimator",
|
||||
key: "estimator",
|
||||
ellipsis: true,
|
||||
responsive: ["xl"],
|
||||
filteredValue: filter?.estimator || null,
|
||||
filterSearch: true,
|
||||
filters:
|
||||
(jobs &&
|
||||
@@ -316,10 +322,11 @@ export function JobsReadyList({ bodyshop }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "N/A",
|
||||
text: s || "No Estimator*",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) =>
|
||||
value.includes(
|
||||
@@ -377,7 +384,7 @@ export function JobsReadyList({ bodyshop }) {
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{ defaultPageSize: 50 }}
|
||||
pagination={{ defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={jobs}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Space, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function JobsRelatedRos({ jobid, job }) {
|
||||
export default function JobsRelatedRos({ jobid, job, disabled }) {
|
||||
if (!(job && job.vehicle && job.vehicle.jobs)) return null;
|
||||
return (
|
||||
<Space wrap>
|
||||
@@ -10,9 +10,15 @@ export default function JobsRelatedRos({ jobid, job }) {
|
||||
.filter((j) => j.id !== job.id)
|
||||
.map((j) => (
|
||||
<Tag key={j.id}>
|
||||
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
|
||||
j.clm_no ? ` | ${j.clm_no}` : ""
|
||||
}${j.status ? ` | ${j.status}` : ""}`}</Link>
|
||||
{disabled ? (
|
||||
<>{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${
|
||||
j.status ? ` | ${j.status}` : ""
|
||||
}`}</>
|
||||
) : (
|
||||
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
|
||||
j.clm_no ? ` | ${j.clm_no}` : ""
|
||||
}${j.status ? ` | ${j.status}` : ""}`}</Link>
|
||||
)}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
export default function OwnersListComponent({
|
||||
loading,
|
||||
@@ -122,7 +123,7 @@ export default function OwnersListComponent({
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 25,
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import AlertComponent from "../alert/alert.component";
|
||||
import OwnersListComponent from "./owners-list.component";
|
||||
import queryString from "query-string";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
export default function OwnersListContainer() {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
@@ -16,8 +17,8 @@ export default function OwnersListContainer() {
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
search: search || "",
|
||||
offset: page ? (page - 1) * 25 : 0,
|
||||
limit: 25,
|
||||
offset: page ? (page - 1) * pageLimit : 0,
|
||||
limit: pageLimit,
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -8,29 +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 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);
|
||||
|
||||
@@ -38,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",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,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"),
|
||||
@@ -124,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 ? (
|
||||
@@ -138,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",
|
||||
@@ -169,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"),
|
||||
@@ -197,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",
|
||||
@@ -296,7 +333,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 50,
|
||||
pageSize: pageLimit,
|
||||
// current: parseInt(page || 1),
|
||||
// total: data && data.jobs_aggregate.aggregate.count,
|
||||
}}
|
||||
@@ -306,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);
|
||||
@@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
await insertPayment({
|
||||
variables: {
|
||||
paymentInput: {
|
||||
amount: -refund_response.data.amount,
|
||||
transactionid: payment_response.response.receiptelements.transid,
|
||||
amount: -refund_response?.data?.amount,
|
||||
transactionid: payment_response?.response?.receiptelements?.transid,
|
||||
payer: record.payer,
|
||||
type: "Refund",
|
||||
jobid: payment_response.jobid,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { alphaSort } from "../../utils/sorters";
|
||||
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -282,11 +283,11 @@ export function PaymentsListPaginated({
|
||||
pagination={
|
||||
search?.search
|
||||
? {
|
||||
pageSize: 25,
|
||||
pageSize: pageLimit,
|
||||
showSizeChanger: false,
|
||||
}
|
||||
: {
|
||||
pageSize: 25,
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
showSizeChanger: false,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Button, Dropdown, Menu } from "antd";
|
||||
import dataSource from "./production-list-columns.data";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import dataSource from "./production-list-columns.data";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -24,6 +24,7 @@ export function ProductionColumnsComponent({
|
||||
columnState,
|
||||
technician,
|
||||
bodyshop,
|
||||
data,
|
||||
tableState,
|
||||
}) {
|
||||
const [columns, setColumns] = columnState;
|
||||
@@ -36,6 +37,7 @@ export function ProductionColumnsComponent({
|
||||
bodyshop,
|
||||
technician,
|
||||
state: tableState,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
}).filter((i) => i.key === e.key),
|
||||
]);
|
||||
@@ -46,6 +48,7 @@ export function ProductionColumnsComponent({
|
||||
technician,
|
||||
state: tableState,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
data: data,
|
||||
});
|
||||
const menu = (
|
||||
<Menu
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { ExclamationCircleFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Dropdown, Menu } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
export default function ProductionListColumnAlert({ record }) {
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
export function ProductionListColumnAlert({ record, insertAuditTrail }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [updateAlert] = useMutation(UPDATE_JOB);
|
||||
@@ -27,6 +38,14 @@ export default function ProductionListColumnAlert({ record }) {
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.alertToggle(
|
||||
!!record.production_vars && !!record.production_vars.alert
|
||||
? !record.production_vars.alert
|
||||
: true
|
||||
),
|
||||
}).then(() => {
|
||||
if (record.refetch) record.refetch();
|
||||
});
|
||||
@@ -58,3 +77,8 @@ export default function ProductionListColumnAlert({ record }) {
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ProductionListColumnAlert);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { PauseCircleOutlined, BranchesOutlined } from "@ant-design/icons";
|
||||
import { Space, Tooltip } from "antd";
|
||||
import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Checkbox, Space, Tooltip } from "antd";
|
||||
import i18n from "i18next";
|
||||
import moment from "moment";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { TimeFormatter } from "../../utils/DateFormatter";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
import ProductionListColumnAlert from "./production-list-columns.alert.component";
|
||||
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
|
||||
@@ -25,7 +28,7 @@ import ProductionListColumnCategory from "./production-list-columns.status.categ
|
||||
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
||||
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
||||
|
||||
const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
|
||||
return [
|
||||
{
|
||||
title: i18n.t("jobs.actions.viewdetail"),
|
||||
@@ -75,8 +78,16 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
dataIndex: "ownr",
|
||||
key: "ownr",
|
||||
ellipsis: true,
|
||||
render: (text, record) => <OwnerNameDisplay ownerObject={record} />,
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
render: (text, record) =>
|
||||
technician ? (
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
) : (
|
||||
<Link to={`/manage/owners/${record.ownerid}`}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</Link>
|
||||
),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -87,18 +98,25 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
a.v_make_desc + a.v_model_desc,
|
||||
b.v_make_desc + b.v_model_desc
|
||||
`${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:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/vehicles/${record.vehicleid}`}>{`${
|
||||
record.v_model_yr || ""
|
||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
|
||||
record.v_color || ""
|
||||
} ${record.plate_no || ""}`}</Link>
|
||||
),
|
||||
render: (text, record) =>
|
||||
technician ? (
|
||||
<>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
} ${record.v_color || ""} ${record.plate_no || ""}`}</>
|
||||
) : (
|
||||
<Link to={`/manage/vehicles/${record.vehicleid}`}>{`${
|
||||
record.v_model_yr || ""
|
||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${
|
||||
record.v_color || ""
|
||||
} ${record.plate_no || ""}`}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.fields.actual_in"),
|
||||
@@ -278,6 +296,20 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
dataIndex: "special_coverage_policy",
|
||||
key: "special_coverage_policy",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
Number(a.special_coverage_policy) - Number(b.special_coverage_policy),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "special_coverage_policy" &&
|
||||
state.sortedInfo.order,
|
||||
filters: [
|
||||
{ text: "True", value: true },
|
||||
{ text: "False", value: false },
|
||||
],
|
||||
onFilter: (value, record) =>
|
||||
value.includes(record.special_coverage_policy),
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.special_coverage_policy} />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -289,6 +321,16 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(bodyshop &&
|
||||
bodyshop.appt_alt_transport.map((s) => {
|
||||
return {
|
||||
text: s,
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
{record.alt_transport}
|
||||
@@ -369,7 +411,11 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
title: i18n.t("production.labels.alert"),
|
||||
dataIndex: "alert",
|
||||
key: "alert",
|
||||
|
||||
sorter: (a, b) =>
|
||||
Number(a.production_vars?.alert || false) -
|
||||
Number(b.production_vars?.alert || false),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alert" && state.sortedInfo.order,
|
||||
render: (text, record) => <ProductionListColumnAlert record={record} />,
|
||||
},
|
||||
{
|
||||
@@ -536,6 +582,36 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
<JobPartsQueueCount parts={record.joblines_status} record={record} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.t("jobs.labels.estimator"),
|
||||
dataIndex: "estimator",
|
||||
key: "estimator",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(),
|
||||
`${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim()
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order,
|
||||
filters:
|
||||
(data &&
|
||||
data
|
||||
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "N/A",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
[],
|
||||
onFilter: (value, record) =>
|
||||
value.includes(
|
||||
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
|
||||
),
|
||||
render: (text, record) =>
|
||||
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
|
||||
},
|
||||
|
||||
//Added as a place holder for St Claude. Not implemented as it requires another join for a field used by only 1 client.
|
||||
// {
|
||||
|
||||
@@ -13,12 +13,14 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||
import JobAtChange from "../job-at-change/job-at-change.component";
|
||||
import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component";
|
||||
import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component";
|
||||
import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component";
|
||||
import CardTemplate from "../job-detail-cards/job-detail-cards.template.component";
|
||||
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
||||
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
@@ -103,7 +105,13 @@ export function ProductionListDetail({
|
||||
{error && <AlertComponent error={JSON.stringify(error)} />}
|
||||
{!loading && data && (
|
||||
<div>
|
||||
<JobEmployeeAssignments job={data.jobs_by_pk} refetch={refetch} />
|
||||
<CardTemplate
|
||||
title={t("jobs.labels.employeeassignments")}
|
||||
loading={loading}
|
||||
>
|
||||
<JobEmployeeAssignments job={data.jobs_by_pk} refetch={refetch} />
|
||||
</CardTemplate>
|
||||
<div style={{ height: "8px" }} />
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label={t("jobs.fields.ro_number")}>
|
||||
{theJob.ro_number || ""}
|
||||
@@ -111,7 +119,7 @@ export function ProductionListDetail({
|
||||
<Descriptions.Item label={t("jobs.fields.alt_transport")}>
|
||||
<Space>
|
||||
{data.jobs_by_pk.alt_transport || ""}
|
||||
<JobAtChange event={{ job: data.jobs_by_pk }} />
|
||||
<JobAtChange job={data.jobs_by_pk} />
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("jobs.fields.clm_no")}>
|
||||
@@ -121,15 +129,30 @@ export function ProductionListDetail({
|
||||
{theJob.ins_co_nm || ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("jobs.fields.owner")}>
|
||||
<OwnerNameDisplay ownerObject={theJob} />
|
||||
<StartChatButton
|
||||
phone={data.jobs_by_pk.ownr_ph1}
|
||||
jobid={data.jobs_by_pk.id}
|
||||
/>
|
||||
<StartChatButton
|
||||
phone={data.jobs_by_pk.ownr_ph2}
|
||||
jobid={data.jobs_by_pk.id}
|
||||
/>
|
||||
<Space>
|
||||
<OwnerNameDisplay ownerObject={theJob} />
|
||||
{!technician ? (
|
||||
<>
|
||||
<StartChatButton
|
||||
phone={data.jobs_by_pk.ownr_ph1}
|
||||
jobid={data.jobs_by_pk.id}
|
||||
/>
|
||||
<StartChatButton
|
||||
phone={data.jobs_by_pk.ownr_ph2}
|
||||
jobid={data.jobs_by_pk.id}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PhoneNumberFormatter>
|
||||
{data.jobs_by_pk.ownr_ph1}
|
||||
</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter>
|
||||
{data.jobs_by_pk.ownr_ph2}
|
||||
</PhoneNumberFormatter>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("jobs.fields.vehicle")}>
|
||||
{`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${
|
||||
@@ -146,21 +169,24 @@ export function ProductionListDetail({
|
||||
<DateFormatter>{theJob.scheduled_completion}</DateFormatter>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<div style={{ height: "8px" }} />
|
||||
<JobDetailCardsPartsComponent
|
||||
loading={loading}
|
||||
data={data ? data.jobs_by_pk : null}
|
||||
/>
|
||||
|
||||
<div style={{ height: "8px" }} />
|
||||
<JobDetailCardsNotesComponent
|
||||
loading={loading}
|
||||
data={data ? data.jobs_by_pk : null}
|
||||
/>
|
||||
{!bodyshop.uselocalmediaserver && (
|
||||
<JobDetailCardsDocumentsComponent
|
||||
loading={loading}
|
||||
data={data ? data.jobs_by_pk : null}
|
||||
/>
|
||||
<>
|
||||
<div style={{ height: "8px" }} />
|
||||
<JobDetailCardsDocumentsComponent
|
||||
loading={loading}
|
||||
data={data ? data.jobs_by_pk : null}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -24,6 +24,7 @@ export function ProductionListTable({
|
||||
technician,
|
||||
currentUser,
|
||||
state,
|
||||
data,
|
||||
setColumns,
|
||||
setState,
|
||||
}) {
|
||||
@@ -41,6 +42,7 @@ export function ProductionListTable({
|
||||
bodyshop,
|
||||
technician,
|
||||
state,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width,
|
||||
@@ -95,6 +97,7 @@ export function ProductionListTable({
|
||||
...ProductionListColumns({
|
||||
technician,
|
||||
state,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Statistic,
|
||||
Table,
|
||||
} from "antd";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import ReactDragListView from "react-drag-listview";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -79,6 +79,7 @@ export function ProductionListTable({
|
||||
bodyshop,
|
||||
technician,
|
||||
state,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width ?? 100,
|
||||
@@ -87,6 +88,33 @@ export function ProductionListTable({
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns =
|
||||
(state &&
|
||||
matchingColumnConfig &&
|
||||
matchingColumnConfig.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
technician,
|
||||
state,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width ?? 100,
|
||||
};
|
||||
})) ||
|
||||
[];
|
||||
setColumns(newColumns);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
//state,
|
||||
matchingColumnConfig,
|
||||
bodyshop,
|
||||
technician,
|
||||
data,
|
||||
]); //State removed from dependency array as it causes race condition when removing columns from table view and is not needed.
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({
|
||||
...state,
|
||||
@@ -104,7 +132,8 @@ export function ProductionListTable({
|
||||
|
||||
const removeColumn = (e) => {
|
||||
const { key } = e;
|
||||
setColumns(columns.filter((i) => i.key !== key));
|
||||
const newColumns = columns.filter((i) => i.key !== key);
|
||||
setColumns(newColumns);
|
||||
};
|
||||
|
||||
const handleResize =
|
||||
@@ -227,6 +256,7 @@ export function ProductionListTable({
|
||||
<ProductionListColumnsAdd
|
||||
columnState={[columns, setColumns]}
|
||||
tableState={state}
|
||||
data={data}
|
||||
/>
|
||||
<ProductionListSaveConfigButton
|
||||
columns={columns}
|
||||
@@ -237,6 +267,7 @@ export function ProductionListTable({
|
||||
state={state}
|
||||
setState={setState}
|
||||
setColumns={setColumns}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
<Input
|
||||
|
||||
@@ -55,10 +55,11 @@ const ret = {
|
||||
"shiftclock:view": 2,
|
||||
|
||||
"shop:config": 4,
|
||||
"shop:rbac": 5,
|
||||
"shop:vendors": 2,
|
||||
"shop:dashboard": 3,
|
||||
"shop:rbac": 5,
|
||||
"shop:reportcenter": 2,
|
||||
"shop:templates": 4,
|
||||
"shop:vendors": 2,
|
||||
|
||||
"temporarydocs:view": 2,
|
||||
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd";
|
||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import {fetchFilterData} from "../../utils/RenderTemplate";
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import {generateInternalReflections} from "./report-center-modal-utils";
|
||||
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
|
||||
|
||||
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
|
||||
return (
|
||||
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
return <RenderFilters form={form} templateId={key} bodyshop={bodyshop}/>;
|
||||
}}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters Section
|
||||
* @param filters
|
||||
* @param form
|
||||
* @param bodyshop
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function FiltersSection({filters, form, bodyshop}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
|
||||
<Form.List name={["filters"]}>
|
||||
{(fields, {add, remove}) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
key={`${index}field`}
|
||||
label={t('reportcenter.labels.advanced_filters_filter_field')}
|
||||
name={[field.name, "field"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
onChange={() => {
|
||||
// Clear related Fields
|
||||
form.setFieldValue(['filters', field.name, 'value'], null);
|
||||
form.setFieldValue(['filters', field.name, 'operator'], null);
|
||||
}}
|
||||
options={
|
||||
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"],['filters', field.name, "value"]]}
|
||||
>
|
||||
{
|
||||
() => {
|
||||
const name = form.getFieldValue(['filters', field.name, "field"]);
|
||||
const type = filters.find(f => f.name === name)?.type;
|
||||
|
||||
return <Form.Item
|
||||
key={`${index}operator`}
|
||||
label={t('reportcenter.labels.advanced_filters_filter_operator')}
|
||||
name={[field.name, "operator"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
options={ getWhereOperatorsByType(type)}
|
||||
onChange={() => {
|
||||
// Clear related Fields
|
||||
|
||||
form.setFieldValue(['filters', field.name, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item dependencies={[
|
||||
['filters', field.name, "field"],
|
||||
['filters', field.name, "operator"]
|
||||
]}
|
||||
>
|
||||
{
|
||||
() => {
|
||||
// Because it looks cleaner than inlining.
|
||||
const name = form.getFieldValue(['filters', field.name, "field"]);
|
||||
const type = filters.find(f => f.name === name)?.type;
|
||||
const reflector = filters.find(f => f.name === name)?.reflector;
|
||||
const operator = form.getFieldValue(['filters', field.name, "operator"]);
|
||||
const operatorType = operator ? getWhereOperatorsByType(type).find((o) => o.value === operator)?.type : null;
|
||||
|
||||
return <Form.Item
|
||||
key={`${index}value`}
|
||||
label={t('reportcenter.labels.advanced_filters_filter_value')}
|
||||
name={[field.name, "value"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{
|
||||
(() => {
|
||||
const generateReflections = (reflector) => {
|
||||
if (!reflector) return [];
|
||||
|
||||
const {name} = reflector;
|
||||
const path = name?.split('.');
|
||||
const upperPath = path?.[0];
|
||||
const finalPath = path?.slice(1).join('.');
|
||||
|
||||
return generateInternalReflections({
|
||||
bodyshop,
|
||||
upperPath,
|
||||
finalPath
|
||||
});
|
||||
};
|
||||
|
||||
const reflections = reflector ? generateReflections(reflector) : [];
|
||||
const fieldPath = [[field.name, "value"]];
|
||||
// We have reflections so we will use a select box
|
||||
if (reflections.length > 0) {
|
||||
// We have reflections and the operator type is array, so we will use a select box with multiple options
|
||||
if (operatorType === "array") {
|
||||
return (
|
||||
<Select
|
||||
disabled={!operator}
|
||||
mode="multiple"
|
||||
options={reflections}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue(fieldPath, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
options={reflections}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue(fieldPath, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// We have a type of number, so we will use a number input
|
||||
if (type === "number") {
|
||||
return (
|
||||
<InputNumber
|
||||
disabled={!operator}
|
||||
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
|
||||
);
|
||||
}
|
||||
|
||||
// We have a type of date, so we will use a date picker
|
||||
if (type === "date") {
|
||||
return (
|
||||
<FormDatePicker
|
||||
disabled={!operator}
|
||||
onChange={(date) => form.setFieldValue(fieldPath, date)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// we have a type of boolean, so we will use a select box with a true or false option.
|
||||
if (type === "boolean" || type === "bool") {
|
||||
return (
|
||||
<Select
|
||||
disabled={!operator}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
options={[
|
||||
{
|
||||
label: t('reportcenter.labels.advanced_filters_true'),
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: t('reportcenter.labels.advanced_filters_false'),
|
||||
value: false
|
||||
}
|
||||
]}
|
||||
onChange={(value) => form.setFieldValue(fieldPath, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
disabled={!operator}
|
||||
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteFilled
|
||||
style={{margin: "1rem", paddingTop: '23px'}}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorters Section
|
||||
* @param sorters
|
||||
* @param form
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function SortersSection({sorters}) {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
|
||||
<Form.List name={["sorters"]}>
|
||||
{(fields, {add, remove}) => {
|
||||
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={t('reportcenter.labels.advanced_filters_sorter_field')}
|
||||
name={[field.name, "field"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={
|
||||
sorters.map((f) => ({
|
||||
value: f.name,
|
||||
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
|
||||
}))
|
||||
}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
key={`${index}direction`}
|
||||
label={t('reportcenter.labels.advanced_filters_sorter_direction')}
|
||||
name={[field.name, "direction"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={getOrderOperatorsByType()}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={2}>
|
||||
<DeleteFilled
|
||||
style={{margin: "1rem", paddingTop: '23px'}}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Filters
|
||||
* @param templateId
|
||||
* @param form
|
||||
* @param bodyshop
|
||||
* @returns {JSX.Element|null}
|
||||
* @constructor
|
||||
*/
|
||||
function RenderFilters({templateId, form, bodyshop}) {
|
||||
const [state, setState] = useState(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {t} = useTranslation();
|
||||
|
||||
const fetch = useCallback(async () => {
|
||||
// Reset all the filters and Sorters.
|
||||
form.resetFields(['filters']);
|
||||
form.resetFields(['sorters']);
|
||||
form.resetFields(['defaultSorters']);
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const data = await fetchFilterData({name: templateId});
|
||||
|
||||
// We have Success
|
||||
if (data?.success) {
|
||||
if (data?.data?.sorters && data?.data?.sorters.length > 0) {
|
||||
const defaultSorters = data?.data?.sorters.filter((sorter) => sorter.hasOwnProperty('default')).map((sorter) => {
|
||||
return {
|
||||
field: sorter.name,
|
||||
direction: sorter.default.direction
|
||||
};
|
||||
}).sort((a, b) => a.default.order - b.default.order);
|
||||
|
||||
form.setFieldValue('defaultSorters', JSON.stringify(defaultSorters));
|
||||
}
|
||||
// Set the state
|
||||
setState(data.data);
|
||||
}
|
||||
// Something went wrong fetching filter data
|
||||
else {
|
||||
setState(null);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [templateId, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (templateId) {
|
||||
fetch();
|
||||
|
||||
}
|
||||
}, [templateId, fetch]);
|
||||
|
||||
const filters = useMemo(() => state?.filters || [], [state]);
|
||||
const sorters = useMemo(() => state?.sorters || [], [state]);
|
||||
|
||||
if (!templateId) return null;
|
||||
if (isLoading) return <LoadingSkeleton/>;
|
||||
if (!state) return null;
|
||||
|
||||
return (
|
||||
<div style={{marginTop: '10px'}}>
|
||||
<Checkbox
|
||||
checked={visible}
|
||||
onChange={(e) => setVisible(e.target.checked)}
|
||||
children={t('reportcenter.labels.advanced_filters')}
|
||||
/>
|
||||
{visible && (
|
||||
<div>
|
||||
{filters.length > 0 && (
|
||||
<FiltersSection filters={filters} form={form} bodyshop={bodyshop}/>
|
||||
)}
|
||||
{sorters.length > 0 && (
|
||||
<SortersSection sorters={sorters} form={form}/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import {uniqBy} from "lodash";
|
||||
|
||||
/**
|
||||
* Get value from path
|
||||
* @param obj
|
||||
* @param path
|
||||
* @returns {*}
|
||||
*/
|
||||
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
|
||||
|
||||
/**
|
||||
* Generate options from array
|
||||
* @param bodyshop
|
||||
* @param path
|
||||
* @returns {unknown[]}
|
||||
*/
|
||||
const generateOptionsFromArray = (bodyshop, path) => {
|
||||
const options = getValueFromPath(bodyshop, path);
|
||||
return uniqBy(options.map((value) => ({
|
||||
label: value,
|
||||
value: value,
|
||||
})), 'value');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Valid internal reflections
|
||||
* Note: This is intended for future functionality
|
||||
* @type {{special: string[], bodyshop: [{name: string, type: string}]}}
|
||||
*/
|
||||
const VALID_INTERNAL_REFLECTIONS = {
|
||||
bodyshop: [
|
||||
{
|
||||
name: 'md_ro_statuses.statuses',
|
||||
type: 'kv-to-v'
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate options
|
||||
* @param bodyshop
|
||||
* @param path
|
||||
* @param labelPath
|
||||
* @param valuePath
|
||||
* @returns {{label: *, value: *}[]}
|
||||
*/
|
||||
const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
|
||||
const options = getValueFromPath(bodyshop, path);
|
||||
return uniqBy(Object.values(options).map((value) => ({
|
||||
label: value[labelPath],
|
||||
value: value[valuePath],
|
||||
})), 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate special reflections
|
||||
* @param bodyshop
|
||||
* @param finalPath
|
||||
* @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
|
||||
*/
|
||||
const generateSpecialReflections = (bodyshop, finalPath) => {
|
||||
switch (finalPath) {
|
||||
// Special case because Referral Sources is an Array, not an Object.
|
||||
case 'referral_source':
|
||||
return generateOptionsFromArray(bodyshop, 'md_referral_sources');
|
||||
case 'class':
|
||||
return generateOptionsFromArray(bodyshop, 'md_classes');
|
||||
case 'cost_centers':
|
||||
return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
|
||||
// Special case because Categories is an Array, not an Object.
|
||||
case 'categories':
|
||||
return generateOptionsFromArray(bodyshop, 'md_categories');
|
||||
case 'insurance_companies':
|
||||
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
|
||||
case 'employee_teams':
|
||||
return generateOptionsFromObject(bodyshop, 'employee_teams', 'name', 'id');
|
||||
// Special case because Employees uses a concatenation of first_name and last_name
|
||||
case 'employees':
|
||||
const employeesOptions = getValueFromPath(bodyshop, 'employees');
|
||||
return uniqBy(Object.values(employeesOptions).map((value) => ({
|
||||
label: `${value.first_name} ${value.last_name}`,
|
||||
value: value.id,
|
||||
})), 'value');
|
||||
case 'last_names':
|
||||
return generateOptionsFromObject(bodyshop, 'employees', 'last_name', 'last_name');
|
||||
case 'first_names':
|
||||
return generateOptionsFromObject(bodyshop, 'employees', 'first_name', 'first_name');
|
||||
case 'job_statuses':
|
||||
const statusOptions = getValueFromPath(bodyshop, 'md_ro_statuses.statuses');
|
||||
return Object.values(statusOptions).map((value) => ({
|
||||
label: value,
|
||||
value
|
||||
}));
|
||||
default:
|
||||
console.error('Invalid Special reflection provided by Report Filters');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate bodyshop reflections
|
||||
* @param bodyshop
|
||||
* @param finalPath
|
||||
* @returns {{label: *, value: *}[]|*[]}
|
||||
*/
|
||||
const generateBodyshopReflections = (bodyshop, finalPath) => {
|
||||
const options = getValueFromPath(bodyshop, finalPath);
|
||||
const reflectionRenderer = VALID_INTERNAL_REFLECTIONS.bodyshop.find(reflection => reflection.name === finalPath);
|
||||
if (reflectionRenderer?.type === 'kv-to-v') {
|
||||
return Object.values(options).map((value) => ({
|
||||
label: value,
|
||||
value
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate internal reflections based on the path and bodyshop
|
||||
* @param bodyshop
|
||||
* @param upperPath
|
||||
* @param finalPath
|
||||
* @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
|
||||
*/
|
||||
const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
|
||||
switch (upperPath) {
|
||||
case 'special':
|
||||
return generateSpecialReflections(bodyshop, finalPath);
|
||||
case 'bodyshop':
|
||||
return generateBodyshopReflections(bodyshop, finalPath);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export {generateInternalReflections}
|
||||
@@ -1,305 +1,321 @@
|
||||
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";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reportCenterModal: selectReportCenter,
|
||||
reportCenterModal: selectReportCenter,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ReportCenterModalComponent);
|
||||
|
||||
export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
|
||||
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") } : {}),
|
||||
const templateConfig = {
|
||||
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,
|
||||
};
|
||||
|
||||
if (_.isString(values.defaultSorters) && !_.isEmpty(values.defaultSorters)) {
|
||||
templateConfig.defaultSorters = JSON.parse(values.defaultSorters);
|
||||
}
|
||||
|
||||
await GenerateDocument(
|
||||
templateConfig,
|
||||
{
|
||||
to: values.to,
|
||||
subject: Templates[values.key]?.subject,
|
||||
},
|
||||
},
|
||||
{
|
||||
to: values.to,
|
||||
subject: Templates[values.key]?.subject,
|
||||
},
|
||||
values.sendbyexcel === "excel"
|
||||
? "x"
|
||||
: values.sendby === "email"
|
||||
? "e"
|
||||
: "p",
|
||||
id
|
||||
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="defaultSorters" hidden/>
|
||||
<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} bodyshop={bodyshop} />
|
||||
<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 (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>
|
||||
// TODO: MERGE NOTE, Ranges turns to presets in DatePicker.RangePicker
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => form.submit()} style={{}} loading={loading}>
|
||||
{t("reportcenter.actions.generate")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
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"
|
||||
ranges={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>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => form.submit()} style={{}} loading={loading}>
|
||||
{t("reportcenter.actions.generate")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectReportCenter } from "../../redux/modals/modals.selectors";
|
||||
import RbacWrapperComponent from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import ReportCenterModalComponent from "./report-center-modal.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -33,7 +34,9 @@ export function ReportCenterModalContainer({
|
||||
destroyOnClose
|
||||
width="80%"
|
||||
>
|
||||
<ReportCenterModalComponent />
|
||||
<RbacWrapperComponent action="shop:reportcenter">
|
||||
<ReportCenterModalComponent />
|
||||
</RbacWrapperComponent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,6 +216,7 @@ export function ScheduleJobModalContainer({
|
||||
okButtonProps={{
|
||||
loading: loading,
|
||||
}}
|
||||
closable={false}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
|
||||
@@ -25,6 +25,8 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
|
||||
return acc + value.bodyhrs;
|
||||
}, 0);
|
||||
|
||||
const numJobs = entries.length;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={moment(date).format("D - ddd")}
|
||||
@@ -33,17 +35,18 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
|
||||
>
|
||||
<Statistic
|
||||
valueStyle={{ color: dailyBodyTarget > bodyHrs ? "red" : "green" }}
|
||||
label="B"
|
||||
label="Body"
|
||||
value={bodyHrs.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
valueStyle={{ color: dailyPaintTarget > paintHrs ? "red" : "green" }}
|
||||
label="P"
|
||||
label="Refinish"
|
||||
value={paintHrs.toFixed(1)}
|
||||
/>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
||||
<Statistic value={(bodyHrs + paintHrs).toFixed(1)} />
|
||||
<Statistic label="Total" value={(bodyHrs + paintHrs).toFixed(1)} />
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Statistic label="Jobs" value={numJobs} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Dropdown, Form, InputNumber, notification } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
Form,
|
||||
InputNumber,
|
||||
notification,
|
||||
Space,
|
||||
} from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
|
||||
@@ -13,6 +22,7 @@ export default function ScoreboardEntryEdit({ entry }) {
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
values.date = moment(values.date).format("YYYY-MM-DD");
|
||||
const result = await updateScoreboardentry({
|
||||
variables: { sbId: entry.id, sbInput: values },
|
||||
});
|
||||
@@ -77,13 +87,14 @@ export default function ScoreboardEntryEdit({ entry }) {
|
||||
>
|
||||
<InputNumber precision={1} />
|
||||
</Form.Item>
|
||||
|
||||
<Button type="primary" loading={loading} htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisible(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
<Space wrap>
|
||||
<Button type="primary" loading={loading} htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisible(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Card, Input, Modal, Space, Table, Typography } from "antd";
|
||||
import React, { useState } from "react";
|
||||
@@ -5,18 +6,21 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
|
||||
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
visible: false,
|
||||
search: "",
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
pageSize: pageLimit,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(
|
||||
@@ -43,6 +47,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.ro_number || t("general.labels.na")}
|
||||
@@ -54,7 +59,11 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
OwnerNameDisplayFunction(a.job),
|
||||
OwnerNameDisplayFunction(b.job)
|
||||
),
|
||||
render: (text, record) => <OwnerNameDisplay ownerObject={record.job} />,
|
||||
},
|
||||
{
|
||||
@@ -62,6 +71,15 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.job.v_model_yr || ""} ${a.job.v_make_desc || ""} ${
|
||||
a.job.v_model_desc || ""
|
||||
}`,
|
||||
`${b.job.v_model_yr || ""} ${b.job.v_make_desc || ""} ${
|
||||
b.job.v_model_desc || ""
|
||||
}`
|
||||
),
|
||||
render: (text, record) => (
|
||||
<span>{`${record.job.v_model_yr || ""} ${
|
||||
record.job.v_make_desc || ""
|
||||
@@ -72,17 +90,20 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
title: t("scoreboard.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.fields.painthrs"),
|
||||
dataIndex: "painthrs",
|
||||
key: "painthrs",
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.fields.bodyhrs"),
|
||||
dataIndex: "bodyhrs",
|
||||
key: "bodyhrs",
|
||||
sorter: (a, b) => Number(a.bodyhrs) - Number(b.bodyhrs),
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.fields.painthrs"),
|
||||
dataIndex: "painthrs",
|
||||
key: "painthrs",
|
||||
sorter: (a, b) => Number(a.painthrs) - Number(b.painthrs),
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
@@ -103,8 +124,9 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
visible={state.visible}
|
||||
destroyOnClose
|
||||
width="80%"
|
||||
closable={false}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
onCancel={() =>
|
||||
onOk={() =>
|
||||
setState((state) => ({
|
||||
...state,
|
||||
visible: false,
|
||||
@@ -148,7 +170,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: state.pageSize || 10,
|
||||
pageSize: state.pageSize || pageLimit,
|
||||
current: state.current || 1,
|
||||
total: data ? data.scoreboard_aggregate.aggregate.count : 0,
|
||||
}}
|
||||
|
||||
@@ -29,10 +29,13 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
let ret = {
|
||||
todayBody: 0,
|
||||
todayPaint: 0,
|
||||
todayJobs: 0,
|
||||
weeklyPaint: 0,
|
||||
weeklyJobs: 0,
|
||||
weeklyBody: 0,
|
||||
toDateBody: 0,
|
||||
toDatePaint: 0,
|
||||
toDateJobs: 0,
|
||||
};
|
||||
|
||||
const today = moment();
|
||||
@@ -40,6 +43,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.todayBody = ret.todayBody + d.bodyhrs;
|
||||
ret.todayPaint = ret.todayPaint + d.painthrs;
|
||||
ret.todayJobs++;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,6 +53,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
|
||||
ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
|
||||
ret.weeklyJobs++;
|
||||
});
|
||||
}
|
||||
StartOfWeek = StartOfWeek.add(1, "day");
|
||||
@@ -60,6 +65,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.toDateBody = ret.toDateBody + d.bodyhrs;
|
||||
ret.toDatePaint = ret.toDatePaint + d.painthrs;
|
||||
ret.toDateJobs++;
|
||||
});
|
||||
}
|
||||
startOfMonth = startOfMonth.add(1, "day");
|
||||
@@ -87,7 +93,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.dailytarget")}
|
||||
value={bodyshop.scoreboard_target.dailyBodyTarget}
|
||||
prefix="B"
|
||||
prefix={t("scoreboard.labels.bodyabbrev")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
@@ -140,7 +146,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={bodyshop.scoreboard_target.dailyPaintTarget}
|
||||
prefix="P"
|
||||
prefix={t("scoreboard.labels.refinishabbrev")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
@@ -181,7 +187,12 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
<Divider style={{ margin: 5 }} />
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={"\u00A0"}
|
||||
prefix={t("scoreboard.labels.total")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.todayPaint + values.todayBody).toFixed(1)}
|
||||
@@ -240,6 +251,29 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Divider style={{ margin: 5 }} />
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={"\u00A0"}
|
||||
prefix={t("scoreboard.labels.jobs")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.todayJobs} />
|
||||
</Col>
|
||||
<Col {...statSpans} />
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.weeklyJobs} />
|
||||
</Col>
|
||||
<Col {...statSpans} />
|
||||
<Col {...statSpans} />
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.toDateJobs} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
@@ -29,7 +29,7 @@ export default connect(
|
||||
|
||||
export function ScoreboardTimeTicketsStats({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const startDate = moment().startOf("month")
|
||||
const startDate = moment().startOf("month");
|
||||
const endDate = moment().endOf("month");
|
||||
|
||||
const fixedPeriods = useMemo(() => {
|
||||
@@ -84,6 +84,8 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) {
|
||||
end: endDate.format("YYYY-MM-DD"),
|
||||
fixedStart: fixedPeriods.start.format("YYYY-MM-DD"),
|
||||
fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"),
|
||||
jobStart: startDate,
|
||||
jobEnd: endDate,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
@@ -340,11 +342,21 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) {
|
||||
larData.push({ ...r, ...lar });
|
||||
});
|
||||
|
||||
const jobData = {};
|
||||
data.jobs.forEach((job) => {
|
||||
job.tthrs = job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
|
||||
});
|
||||
jobData.tthrs = data.jobs
|
||||
.reduce((acc, val) => acc + val.tthrs, 0)
|
||||
.toFixed(1);
|
||||
jobData.count = data.jobs.length.toFixed(0);
|
||||
|
||||
return {
|
||||
fixed: ret,
|
||||
combinedData: combinedData,
|
||||
labData: labData,
|
||||
larData: larData,
|
||||
jobData: jobData,
|
||||
};
|
||||
}, [fixedPeriods, data, bodyshop]);
|
||||
|
||||
@@ -356,7 +368,10 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) {
|
||||
<ScoreboardTimeticketsTargetsTable />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ScoreboardTicketsStats data={calculatedData.fixed} />
|
||||
<ScoreboardTicketsStats
|
||||
data={calculatedData.fixed}
|
||||
jobData={calculatedData.jobData}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<ScoreboardTimeTicketsChart
|
||||
|
||||
@@ -41,7 +41,7 @@ function useLocalStorage(key, initialValue) {
|
||||
return [storedValue, setStoredValue];
|
||||
}
|
||||
|
||||
export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
export function ScoreboardTicketsStats({ data, jobData, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [isLarge, setIsLarge] = useLocalStorage("isLargeStatistic", false);
|
||||
|
||||
@@ -408,7 +408,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
{/* Monthly Stats */}
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* This Month */}
|
||||
<Col span={8} align="center">
|
||||
<Col span={7} align="center">
|
||||
<Card size="small" title={t("scoreboard.labels.thismonth")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
@@ -482,7 +482,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
</Card>
|
||||
</Col>
|
||||
{/* Last Month */}
|
||||
<Col span={8} align="center">
|
||||
<Col span={7} align="center">
|
||||
<Card size="small" title={t("scoreboard.labels.lastmonth")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
@@ -556,7 +556,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
</Card>
|
||||
</Col>
|
||||
{/* Efficiency Over Period */}
|
||||
<Col span={8} align="center">
|
||||
<Col span={7} align="center">
|
||||
<Card
|
||||
size="small"
|
||||
title={t("scoreboard.labels.efficiencyoverperiod")}
|
||||
@@ -604,6 +604,40 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={3} align="center">
|
||||
<Card
|
||||
size="small"
|
||||
title={t("scoreboard.labels.jobscompletednotinvoiced")}
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Statistic
|
||||
value={jobData.count}
|
||||
valueStyle={{
|
||||
fontSize: statisticSize,
|
||||
fontWeight: statisticWeight,
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Statistic
|
||||
title={
|
||||
<Typography.Text strong>
|
||||
{t("scoreboard.labels.totalhrs")}
|
||||
</Typography.Text>
|
||||
}
|
||||
value={jobData.tthrs}
|
||||
valueStyle={{
|
||||
fontSize: statisticSize,
|
||||
fontWeight: statisticWeight,
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
{/* Disclaimer */}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user