Compare commits
116 Commits
vite
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc25c23982 | ||
|
|
2215c8439e | ||
|
|
013b56778b | ||
|
|
9dec4a3a61 | ||
|
|
189b4db90f | ||
|
|
c49fa1c527 | ||
|
|
53ef048f6f | ||
|
|
1cc7eed983 | ||
|
|
1c8f377212 | ||
|
|
c050947276 | ||
|
|
9bde1f820d | ||
|
|
393765640c | ||
|
|
69c2836425 | ||
|
|
ec9edf30eb | ||
|
|
6a812f9ea7 | ||
|
|
fa86254bfd | ||
|
|
58ab7afbb3 | ||
|
|
fa7d90d2a9 | ||
|
|
fbf9047974 | ||
|
|
3e9b795052 | ||
|
|
d29ffc21e5 | ||
|
|
959f7780e8 | ||
|
|
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 |
@@ -42,7 +42,7 @@ jobs:
|
||||
app-build:
|
||||
docker:
|
||||
- image: cimg/node:16.15.0
|
||||
|
||||
resource_class: large
|
||||
working_directory: ~/repo/client
|
||||
|
||||
steps:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
test-app-build:
|
||||
docker:
|
||||
- image: cimg/node:16.15.0
|
||||
|
||||
resource_class: large
|
||||
working_directory: ~/repo/client
|
||||
|
||||
steps:
|
||||
@@ -159,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,
|
||||
};
|
||||
189
_reference/reportFiltersAndSorters.md
Normal file
189
_reference/reportFiltersAndSorters.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# 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`
|
||||
- `special.lost_sale_reasons` - This will reflect the lost sale reasons `bodyshop.md_lost_sale_reasons`
|
||||
- `special.alt_transports` - This will reflect the alternative transports `bodyshop.appt_alt_transport`
|
||||
- `special.payment_types` - This will reflect the payment types `bodyshop.md_payment_types`
|
||||
- `special.payment_payers` - This is a special case with a key value set of [Customer, Insurance]
|
||||
|
||||
### 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.
|
||||
- *INHERITANCE CAVEAT* If you have a filters file on a parent report that has a child that you do not want the filters inherited from, you must place a blank filters file (valid json so {}) on the child report level. This will than fetch the child filters, which are empty and move along, versus inheriting the parent filters.
|
||||
|
||||
## 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,21 +1,21 @@
|
||||
import { Input, Table, Checkbox, Card, Space } from "antd";
|
||||
import { Card, Checkbox, Input, Space, Table } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import queryString from "query-string";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -138,7 +138,6 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
|
||||
render: (text, record) => (
|
||||
<ExportLogsCountDisplay logs={record.exportlogs} />
|
||||
),
|
||||
@@ -147,8 +146,6 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
render: (text, record) => (
|
||||
<PayableExportButton
|
||||
billId={record.id}
|
||||
|
||||
@@ -8,14 +8,16 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||
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,
|
||||
@@ -75,7 +77,11 @@ export function AccountingPayablesTableComponent({
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
OwnerNameDisplayFunction(a.job),
|
||||
OwnerNameDisplayFunction(b.job)
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
@@ -94,6 +100,9 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("payments.fields.amount"),
|
||||
dataIndex: "amount",
|
||||
key: "amount",
|
||||
sorter: (a, b) => a.amount - b.amount,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||
),
|
||||
@@ -112,18 +121,21 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("payments.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.exportedat"),
|
||||
dataIndex: "exportedat",
|
||||
key: "exportedat",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// title: t("payments.fields.exportedat"),
|
||||
// dataIndex: "exportedat",
|
||||
// key: "exportedat",
|
||||
// render: (text, record) => (
|
||||
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
@@ -137,8 +149,6 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
render: (text, record) => (
|
||||
<PaymentExportButton
|
||||
paymentId={record.id}
|
||||
|
||||
@@ -4,17 +4,19 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -63,7 +65,7 @@ export function AccountingReceivablesTableComponent({
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => a.status - b.status,
|
||||
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -83,7 +85,8 @@ export function AccountingReceivablesTableComponent({
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
@@ -103,6 +106,15 @@ export function AccountingReceivablesTableComponent({
|
||||
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}>
|
||||
|
||||
@@ -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, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
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,11 @@ 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),
|
||||
type: "billdeleted",
|
||||
});
|
||||
|
||||
if (callback && typeof callback === "function") callback(bill.id);
|
||||
} else {
|
||||
|
||||
@@ -33,8 +33,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
@@ -150,6 +150,7 @@ export function BillDetailEditcontainer({
|
||||
jobid: bill.jobid,
|
||||
billid: search.billid,
|
||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||
type: "billupdated",
|
||||
});
|
||||
|
||||
await refetch();
|
||||
|
||||
@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||
insertAuditTrail: ({ jobid, billid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, billid, operation })),
|
||||
insertAuditTrail: ({ jobid, billid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, billid, operation, type })),
|
||||
});
|
||||
|
||||
const Templates = TemplateList("job_special");
|
||||
@@ -94,6 +94,7 @@ function BillEnterModalContainer({
|
||||
location,
|
||||
outstanding_returns,
|
||||
inventory,
|
||||
federal_tax_exempt,
|
||||
...remainingValues
|
||||
} = values;
|
||||
|
||||
@@ -170,6 +171,7 @@ function BillEnterModalContainer({
|
||||
mod_lbr_ty: key,
|
||||
hours: adjustmentsToInsert[key].toFixed(1),
|
||||
}),
|
||||
type: "jobmodifylbradj",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -319,6 +321,7 @@ function BillEnterModalContainer({
|
||||
operation: AuditTrailMapping.billposted(
|
||||
r1.data.insert_bills.returning[0].invoice_number
|
||||
),
|
||||
type: "billposted",
|
||||
});
|
||||
|
||||
if (enterAgain) {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
||||
});
|
||||
|
||||
@@ -102,6 +102,7 @@ const CardPaymentModalComponent = ({
|
||||
insertAuditTrail({
|
||||
jobid: payment.jobid,
|
||||
operation: AuditTrailMapping.failedpayment(),
|
||||
type: "failedpayment",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,7 +78,8 @@ 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 ? nextservicekm <= mileage : false;
|
||||
|
||||
@@ -80,11 +87,25 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
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>
|
||||
)}
|
||||
@@ -97,6 +118,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
dataIndex: "readiness",
|
||||
key: "readiness",
|
||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||
filteredValue: filter?.readiness || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.readiness.ready"),
|
||||
@@ -212,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,9 +5,11 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function CsiResponseListPaginated({
|
||||
refetch,
|
||||
@@ -16,23 +18,23 @@ export default function CsiResponseListPaginated({
|
||||
total,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { responseid, page, sortcolumn, sortorder } = search;
|
||||
const { responseid } = search;
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
page: "",
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
width: "8%",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.ro_number || t("general.labels.na")}
|
||||
@@ -41,15 +43,18 @@ export default function CsiResponseListPaginated({
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||
width: "25%",
|
||||
sortOrder: sortcolumn === "owner" && sortorder,
|
||||
dataIndex: "owner_name",
|
||||
key: "owner_name",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
OwnerNameDisplayFunction(a.job),
|
||||
OwnerNameDisplayFunction(b.job)
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.job.owner ? (
|
||||
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||
return record.job.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.job.ownerid}>
|
||||
<OwnerNameDisplay ownerObject={record.job} />
|
||||
</Link>
|
||||
) : (
|
||||
@@ -64,9 +69,9 @@ export default function CsiResponseListPaginated({
|
||||
dataIndex: "completedon",
|
||||
key: "completedon",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.completedon - b.completedon,
|
||||
width: "25%",
|
||||
sortOrder: sortcolumn === "completedon" && sortorder,
|
||||
sorter: (a, b) => dateSort(a.completedon, b.completedon),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.completedon ? (
|
||||
<DateFormatter>{record.completedon}</DateFormatter>
|
||||
@@ -76,11 +81,12 @@ export default function CsiResponseListPaginated({
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
setState({
|
||||
...state,
|
||||
filteredInfo: filters,
|
||||
sortedInfo: sorter,
|
||||
page: pagination.current,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
@@ -108,7 +114,7 @@ export default function CsiResponseListPaginated({
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
current: parseInt(state.page || 1),
|
||||
total: total,
|
||||
}}
|
||||
columns={columns}
|
||||
@@ -122,13 +128,6 @@ export default function CsiResponseListPaginated({
|
||||
selectedRowKeys: [responseid],
|
||||
type: "radio",
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleOnRowClick(record);
|
||||
}, // click row
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -3,21 +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";
|
||||
import {pageLimit} from "../../../utils/config";
|
||||
|
||||
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} />;
|
||||
@@ -31,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,
|
||||
@@ -49,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);
|
||||
@@ -59,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}
|
||||
@@ -91,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
|
||||
@@ -108,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>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -134,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>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -142,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
|
||||
@@ -165,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: pageLimit }}
|
||||
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>
|
||||
@@ -220,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,37 +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";
|
||||
import {pageLimit} from "../../../utils/config";
|
||||
|
||||
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}
|
||||
@@ -61,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
|
||||
@@ -78,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>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -104,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>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -112,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
|
||||
@@ -135,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: pageLimit }}
|
||||
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>
|
||||
@@ -188,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
|
||||
@@ -200,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%;
|
||||
|
||||
@@ -53,6 +53,7 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
|
||||
insertAuditTrail({
|
||||
jobid: event.job.id,
|
||||
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||
type: "appointmentcancel",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobChecklistForm({
|
||||
@@ -183,6 +183,7 @@ export function JobChecklistForm({
|
||||
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
|
||||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
|
||||
),
|
||||
type: "jobchecklist",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
|
||||
@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -46,6 +46,7 @@ export function JobEmployeeAssignmentsContainer({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobassignmentchange(operation, name),
|
||||
type: "jobassignmentchange",
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
@@ -76,6 +77,7 @@ export function JobEmployeeAssignmentsContainer({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobassignmentremoved(operation),
|
||||
type: "jobassignmentremoved",
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -113,6 +113,7 @@ export function JobLineConvertToLabor({
|
||||
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
|
||||
mod_lbr_ty,
|
||||
}),
|
||||
type: "jobmodifylbradj",
|
||||
});
|
||||
setLoading(false);
|
||||
setVisibility(false);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
|
||||
|
||||
@@ -32,6 +32,7 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobstatuschange(status),
|
||||
type: "admin_jobstatuschange",
|
||||
});
|
||||
// refetch();
|
||||
})
|
||||
|
||||
@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
@@ -57,6 +57,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
||||
? DateTimeFormat(changedAuditFields[key])
|
||||
: changedAuditFields[key]
|
||||
),
|
||||
type: "admin_jobfieldchange",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -59,6 +59,7 @@ export function JobAdminMarkReexport({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobmarkforreexport(),
|
||||
type: "admin_jobmarkforreexport",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
@@ -99,6 +100,7 @@ export function JobAdminMarkReexport({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobmarkexported(),
|
||||
type: "admin_jobmarkexported",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
@@ -124,6 +126,7 @@ export function JobAdminMarkReexport({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobuninvoice(),
|
||||
type: "admin_jobuninvoice",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
|
||||
@@ -10,8 +10,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
|
||||
@@ -34,6 +34,7 @@ export function JobsAdminRemoveAR({ insertAuditTrail, job }) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_job_remove_from_ar(value),
|
||||
type: "admin_job_remove_from_ar",
|
||||
});
|
||||
setSwitchValue(value);
|
||||
} else {
|
||||
|
||||
@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
|
||||
|
||||
@@ -49,6 +49,7 @@ export function JobsAdminUnvoid({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobunvoid(),
|
||||
type: "admin_jobunvoid",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
|
||||
@@ -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";
|
||||
@@ -47,8 +47,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export function JobsAvailableContainer({
|
||||
bodyshop,
|
||||
@@ -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);
|
||||
|
||||
@@ -182,6 +190,7 @@ export function JobsAvailableContainer({
|
||||
insertAuditTrail({
|
||||
jobid: r.data.insert_jobs.returning[0].id,
|
||||
operation: AuditTrailMapping.jobimported(),
|
||||
type: "jobimported",
|
||||
});
|
||||
|
||||
deleteJob({
|
||||
@@ -197,11 +206,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 +239,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 +291,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) {
|
||||
@@ -321,6 +351,7 @@ export function JobsAvailableContainer({
|
||||
insertAuditTrail({
|
||||
jobid: selectedJob,
|
||||
operation: AuditTrailMapping.jobsupplement(),
|
||||
type: "jobsupplement",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -367,7 +398,6 @@ export function JobsAvailableContainer({
|
||||
|
||||
if (error) return <AlertComponent type="error" message={error.message} />;
|
||||
|
||||
|
||||
return (
|
||||
<LoadingSpinner
|
||||
loading={insertLoading}
|
||||
@@ -384,7 +414,6 @@ export function JobsAvailableContainer({
|
||||
visible={ownerModalVisible}
|
||||
onOk={onOwnerFindModalOk}
|
||||
onCancel={onOwnerModalCancel}
|
||||
|
||||
/>
|
||||
<JobsFindModalContainer
|
||||
loading={estDataRaw.loading}
|
||||
@@ -398,6 +427,8 @@ export function JobsAvailableContainer({
|
||||
modalSearchState={modalSearchState}
|
||||
partsQueueToggle={partsQueueToggle}
|
||||
setPartsQueueToggle={setPartsQueueToggle}
|
||||
updateSchComp={updateSchComp}
|
||||
setSchComp={setSchComp}
|
||||
/>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
|
||||
@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
@@ -35,6 +35,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(status),
|
||||
type: "jobstatuschange",
|
||||
});
|
||||
// refetch();
|
||||
})
|
||||
|
||||
@@ -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, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
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,11 @@ export function JobsCloseExportButton({
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
|
||||
);
|
||||
@@ -192,12 +205,21 @@ 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(),
|
||||
type: "jobexported",
|
||||
});
|
||||
updateJobCache([
|
||||
...new Set(
|
||||
successfulTransactions.map(
|
||||
@@ -227,4 +249,7 @@ export function JobsCloseExportButton({
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(JobsCloseExportButton);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsCloseExportButton);
|
||||
|
||||
@@ -25,8 +25,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsConvertButton({
|
||||
@@ -78,6 +78,7 @@ export function JobsConvertButton({
|
||||
operation: AuditTrailMapping.jobconverted(
|
||||
res.data.update_jobs.returning[0].ro_number
|
||||
),
|
||||
type: "jobconverted",
|
||||
});
|
||||
|
||||
setVisible(false);
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function AddToProduction(
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobinproductionchange(!remove),
|
||||
type: "jobinproductionchange",
|
||||
})
|
||||
);
|
||||
if (completionCallback) completionCallback();
|
||||
|
||||
@@ -52,8 +52,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
setCardPaymentContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsDetailHeaderActions({
|
||||
@@ -115,6 +115,7 @@ export function JobsDetailHeaderActions({
|
||||
? !job.production_vars.alert
|
||||
: true
|
||||
),
|
||||
type: "alertToggle",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -129,6 +130,13 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
},
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobsuspend(
|
||||
!!job.suspended ? !job.suspended : true
|
||||
),
|
||||
type: "jobsuspend",
|
||||
});
|
||||
};
|
||||
|
||||
const statusmenu = (
|
||||
@@ -184,6 +192,7 @@ export function JobsDetailHeaderActions({
|
||||
jobid: job.id,
|
||||
operation:
|
||||
AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||
type: "appointmentcancel",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -540,6 +549,11 @@ export function JobsDetailHeaderActions({
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.voided"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobvoid(),
|
||||
type: "jobvoid",
|
||||
});
|
||||
//go back to jobs list.
|
||||
history.push(`/manage/`);
|
||||
} else {
|
||||
|
||||
@@ -123,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>
|
||||
|
||||
@@ -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, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
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,13 @@ export function JobsExportAllButton({
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map(
|
||||
(job) => job.id
|
||||
@@ -190,13 +205,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 +226,15 @@ export function JobsExportAllButton({
|
||||
]
|
||||
)
|
||||
),
|
||||
]);
|
||||
];
|
||||
if (successfulTransactionsSet.length > 0) {
|
||||
insertAuditTrail({
|
||||
jobid: successfulTransactionsSet[0],
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
}
|
||||
updateJobCache(successfulTransactionsSet);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -225,4 +252,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>
|
||||
|
||||
@@ -21,6 +21,7 @@ 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";
|
||||
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -145,7 +146,8 @@ export function JobsList({ bodyshop }) {
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
@@ -188,7 +190,8 @@ export function JobsList({ bodyshop }) {
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sorter: (a, b) =>
|
||||
statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filteredValue: filter?.status || null,
|
||||
@@ -219,6 +222,15 @@ export function JobsList({ bodyshop }) {
|
||||
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
|
||||
@@ -266,6 +278,9 @@ export function JobsList({ bodyshop }) {
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
|
||||
filteredValue: filter?.ins_co_nm || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
@@ -302,6 +317,13 @@ export function JobsList({ bodyshop }) {
|
||||
key: "estimator",
|
||||
ellipsis: true,
|
||||
responsive: ["xl"],
|
||||
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,
|
||||
filterSearch: true,
|
||||
filteredValue: filter?.estimator || null,
|
||||
filters:
|
||||
|
||||
@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer);
|
||||
|
||||
@@ -49,6 +49,7 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobnotedeleted(),
|
||||
type: "jobnotedeleted",
|
||||
});
|
||||
});
|
||||
setDeleteLoading(false);
|
||||
|
||||
@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -76,6 +76,7 @@ export function LaborAllocationsAdjustmentEdit({
|
||||
values.hours -
|
||||
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
|
||||
}),
|
||||
type: "jobmodifylbradj",
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function NoteUpsertModalContainer({
|
||||
@@ -70,6 +70,7 @@ export function NoteUpsertModalContainer({
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteupdated(),
|
||||
type: "jobnoteupdated",
|
||||
});
|
||||
});
|
||||
if (refetch) refetch();
|
||||
@@ -102,6 +103,7 @@ export function NoteUpsertModalContainer({
|
||||
insertAuditTrail({
|
||||
jobid: newJobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
type: "jobnoteadded",
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -115,6 +117,7 @@ export function NoteUpsertModalContainer({
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
type: "jobnoteadded",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useMutation, useQuery, useApolloClient } from "@apollo/client";
|
||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Form, Modal, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent, auth } from "../../firebase/firebase.utils";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import {
|
||||
INSERT_NEW_PARTS_ORDERS,
|
||||
QUERY_PARTS_ORDER_OEC,
|
||||
@@ -29,10 +33,6 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import PartsOrderModalComponent from "./parts-order-modal.component";
|
||||
import axios from "axios";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import _ from "lodash";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -45,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function PartsOrderModalContainer({
|
||||
@@ -142,6 +142,7 @@ export function PartsOrderModalContainer({
|
||||
: AuditTrailMapping.jobspartsorder(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
),
|
||||
type: isReturn ? "jobspartsreturn" : "jobspartsorder",
|
||||
});
|
||||
|
||||
const jobLinesResult = await updateJobLines({
|
||||
|
||||
@@ -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,30 +8,28 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component";
|
||||
import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component";
|
||||
import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component";
|
||||
import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component";
|
||||
import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function PartsQueuePageComponent({ bodyshop }) {
|
||||
export function PartsQueueListComponent({ bodyshop }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const {
|
||||
//page,
|
||||
sortcolumn,
|
||||
sortorder,
|
||||
statusFilters,
|
||||
} = searchParams;
|
||||
const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
|
||||
const history = useHistory();
|
||||
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
|
||||
|
||||
@@ -39,19 +37,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
// offset: page ? (page - 1) * 25 : 0,
|
||||
// limit: 25,
|
||||
statuses: (statusFilters && JSON.parse(statusFilters)) ||
|
||||
bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "ro_number"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -107,6 +94,19 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
history.push({ search: queryString.stringify(searchParams) });
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
history.push({
|
||||
search: queryString.stringify({
|
||||
...searchParams,
|
||||
selected: record.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
@@ -125,7 +125,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "ownr_ln",
|
||||
key: "ownr_ln",
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
@@ -139,6 +140,56 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder: sortcolumn === "vehicle" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm_short"),
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder: sortcolumn === "ins_co_nm" && sortorder,
|
||||
filteredValue: filter?.ins_co_nm || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
.map((j) => j.ins_co_nm)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Ins. Co.*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.ins_co_nm),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
@@ -170,23 +221,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
sorter: (a, b) =>
|
||||
dateSort(a.scheduled_completion, b.scheduled_completion),
|
||||
sortOrder: sortcolumn === "scheduled_completion" && sortorder,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// title: t("vehicles.fields.plate_no"),
|
||||
@@ -198,14 +242,6 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
// return record.plate_no ? record.plate_no : "";
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||
},
|
||||
// {
|
||||
// title: t("jobs.fields.clm_total"),
|
||||
// dataIndex: "clm_total",
|
||||
@@ -307,9 +343,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
style={{ height: "100%" }}
|
||||
scroll={{ x: true }}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record) => {
|
||||
handleOnRowClick(record);
|
||||
},
|
||||
selectedRowKeys: [selected],
|
||||
type: "radio",
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(PartsQueuePageComponent);
|
||||
export default connect(mapStateToProps, null)(PartsQueueListComponent);
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Button, Form, Input, PageHeader, Space } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectAuthLevel,
|
||||
selectBodyshop,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||
import PhoneFormItem, {
|
||||
@@ -8,12 +14,6 @@ import PhoneFormItem, {
|
||||
} from "../form-items-formatted/phone-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectAuthLevel,
|
||||
selectBodyshop,
|
||||
} from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
authLevel: selectAuthLevel,
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -46,13 +46,19 @@ export function PhonebookFormComponent({
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={`${form.getFieldValue("firstname") || ""} ${
|
||||
form.getFieldValue("lastname") || ""
|
||||
}${
|
||||
form.getFieldValue("company")
|
||||
? ` - ${form.getFieldValue("company")}`
|
||||
: ""
|
||||
}`}
|
||||
title={
|
||||
<Form.Item shouldUpdate>
|
||||
{() =>
|
||||
`${form.getFieldValue("firstname") || ""} ${
|
||||
form.getFieldValue("lastname") || ""
|
||||
}${
|
||||
form.getFieldValue("company")
|
||||
? ` - ${form.getFieldValue("company")}`
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
|
||||
@@ -29,8 +29,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionBoardKanbanComponent({
|
||||
@@ -133,6 +133,7 @@ export function ProductionBoardKanbanComponent({
|
||||
insertAuditTrail({
|
||||
jobid: card.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
|
||||
type: "jobstatuschange",
|
||||
});
|
||||
|
||||
if (update.errors) {
|
||||
|
||||
@@ -13,8 +13,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionListColumnAlert({ record, insertAuditTrail }) {
|
||||
@@ -46,6 +46,7 @@ export function ProductionListColumnAlert({ record, insertAuditTrail }) {
|
||||
? !record.production_vars.alert
|
||||
: true
|
||||
),
|
||||
type: "alertToggle",
|
||||
}).then(() => {
|
||||
if (record.refetch) record.refetch();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Space, Tooltip } from "antd";
|
||||
import { Checkbox, Space, Tooltip } from "antd";
|
||||
import i18n from "i18next";
|
||||
import moment from "moment";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -10,7 +10,9 @@ 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";
|
||||
@@ -84,7 +86,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</Link>
|
||||
),
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -95,8 +98,10 @@ const r = ({ technician, state, activeStatuses, data, 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,
|
||||
@@ -291,6 +296,20 @@ const r = ({ technician, state, activeStatuses, data, 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} />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -302,6 +321,16 @@ const r = ({ technician, state, activeStatuses, data, 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}
|
||||
@@ -382,7 +411,11 @@ const r = ({ technician, state, activeStatuses, data, 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} />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,12 +3,12 @@ import { useMutation } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
notification,
|
||||
Popover,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
notification,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -25,8 +25,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionListEmpAssignment({
|
||||
@@ -55,6 +55,7 @@ export function ProductionListEmpAssignment({
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobassignmentchange(empAssignment, name),
|
||||
type: "jobassignmentchange",
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
@@ -80,6 +81,7 @@ export function ProductionListEmpAssignment({
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobassignmentremoved(empAssignment),
|
||||
type: "jobassignmentremoved",
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
|
||||
@@ -12,8 +12,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export function ProductionListColumnCategory({ record, bodyshop }) {
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
|
||||
@@ -5,16 +5,16 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export function ProductionListColumnStatus({
|
||||
record,
|
||||
@@ -40,6 +40,7 @@ export function ProductionListColumnStatus({
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(key),
|
||||
type: "jobstatuschange",
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
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,
|
||||
t
|
||||
});
|
||||
};
|
||||
|
||||
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,161 @@
|
||||
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
|
||||
* @param t - i18n
|
||||
* @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
|
||||
*/
|
||||
const generateSpecialReflections = (bodyshop, finalPath, t) => {
|
||||
switch (finalPath) {
|
||||
case 'payment_payers':
|
||||
return [
|
||||
{
|
||||
label: t("payments.labels.customer"),
|
||||
value: t("payments.labels.customer"),
|
||||
},
|
||||
{
|
||||
label: t("payments.labels.insurance"),
|
||||
value: t("payments.labels.insurance"),
|
||||
},
|
||||
// This is a weird one supposedly only used by one shop and could potentially be
|
||||
// placed behind a SplitSDK
|
||||
{
|
||||
label: t("payments.labels.external"),
|
||||
value: t("payments.labels.external"),
|
||||
}
|
||||
];
|
||||
case 'payment_types':
|
||||
return generateOptionsFromArray(bodyshop, 'md_payment_types');
|
||||
case 'alt_transports':
|
||||
return generateOptionsFromArray(bodyshop, 'appt_alt_transport');
|
||||
case 'lost_sale_reasons':
|
||||
return generateOptionsFromArray(bodyshop, 'md_lost_sale_reasons');
|
||||
// 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
|
||||
* @param t - i18n
|
||||
* @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
|
||||
*/
|
||||
const generateInternalReflections = ({bodyshop, upperPath, finalPath, t}) => {
|
||||
switch (upperPath) {
|
||||
case 'special':
|
||||
return generateSpecialReflections(bodyshop, finalPath, t);
|
||||
case 'bodyshop':
|
||||
return generateBodyshopReflections(bodyshop, finalPath);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export {generateInternalReflections}
|
||||
@@ -1,70 +1,64 @@
|
||||
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);
|
||||
@@ -72,244 +66,256 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
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 style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
const datedisable = Templates[key] && Templates[key].datedisable;
|
||||
if (datedisable !== true) {
|
||||
return (
|
||||
<Form.Item
|
||||
name="dates"
|
||||
label={t("reportcenter.labels.dates")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker.RangePicker
|
||||
format="MM/DD/YYYY"
|
||||
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;
|
||||
}}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ScheduleJobModalContainer({
|
||||
@@ -146,6 +146,7 @@ export function ScheduleJobModalContainer({
|
||||
operation: AuditTrailMapping.appointmentinsert(
|
||||
DateTimeFormat(values.start)
|
||||
),
|
||||
type: "appointmentinsert",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,12 +6,14 @@ 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";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
@@ -44,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")}
|
||||
@@ -55,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} />,
|
||||
},
|
||||
{
|
||||
@@ -63,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 || ""
|
||||
@@ -73,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"),
|
||||
@@ -104,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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Form } from "antd";
|
||||
import React from "react";
|
||||
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
||||
|
||||
export default function ShopCsiConfigForm({ selectedCsi }) {
|
||||
@@ -9,7 +9,7 @@ export default function ShopCsiConfigForm({ selectedCsi }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
The Config Form {readOnly}
|
||||
{readOnly}
|
||||
{selectedCsi && (
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<ConfigFormComponents
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CheckCircleFilled } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Col, List, Row } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GET_ALL_QUESTION_SETS } from "../../graphql/csi.queries";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
@@ -21,7 +21,6 @@ export default function ShopCsiConfig() {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<div>
|
||||
The Config Form
|
||||
<Row>
|
||||
<Col span={3}>
|
||||
<List
|
||||
@@ -42,7 +41,8 @@ export default function ShopCsiConfig() {
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Col span={1} />
|
||||
<Col span={20}>
|
||||
<ShopCsiConfigForm selectedCsi={selectedCsi} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -40,6 +40,7 @@ export const INSERT_AUDIT_TRAIL = gql`
|
||||
bodyshopid
|
||||
created
|
||||
operation
|
||||
type
|
||||
useremail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export const QUERY_AVAILABLE_CC = gql`
|
||||
]
|
||||
status: { _eq: "courtesycars.status.in" }
|
||||
}
|
||||
order_by: { fleetnumber: asc }
|
||||
) {
|
||||
color
|
||||
dailycost
|
||||
@@ -29,6 +30,7 @@ export const QUERY_AVAILABLE_CC = gql`
|
||||
fleetnumber
|
||||
fuel
|
||||
id
|
||||
insuranceexpires
|
||||
make
|
||||
mileage
|
||||
model
|
||||
@@ -57,7 +59,7 @@ export const CHECK_CC_FLEET_NUMBER = gql`
|
||||
`;
|
||||
export const QUERY_ALL_CC = gql`
|
||||
query QUERY_ALL_CC {
|
||||
courtesycars {
|
||||
courtesycars(order_by: { fleetnumber: asc }) {
|
||||
color
|
||||
created_at
|
||||
dailycost
|
||||
|
||||
@@ -57,19 +57,15 @@ export const INSERT_CSI = gql`
|
||||
`;
|
||||
|
||||
export const QUERY_CSI_RESPONSE_PAGINATED = gql`
|
||||
query QUERY_CSI_RESPONSE_PAGINATED(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$order: [csi_order_by!]!
|
||||
) {
|
||||
csi(offset: $offset, limit: $limit, order_by: $order) {
|
||||
query QUERY_CSI_RESPONSE_PAGINATED {
|
||||
csi(order_by: { completedon: desc_nulls_last }) {
|
||||
id
|
||||
completedon
|
||||
job {
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownerid
|
||||
ro_number
|
||||
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -83,6 +79,7 @@ export const QUERY_CSI_RESPONSE_PAGINATED = gql`
|
||||
export const QUERY_CSI_RESPONSE_BY_PK = gql`
|
||||
query QUERY_CSI_RESPONSE_BY_PK($id: uuid!) {
|
||||
csi_by_pk(id: $id) {
|
||||
completedon
|
||||
relateddata
|
||||
valid
|
||||
validuntil
|
||||
|
||||
@@ -108,12 +108,7 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
|
||||
`;
|
||||
|
||||
export const QUERY_PARTS_QUEUE = gql`
|
||||
query QUERY_PARTS_QUEUE(
|
||||
$statuses: [String!]!
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$order: [jobs_order_by!]
|
||||
) {
|
||||
query QUERY_PARTS_QUEUE($statuses: [String!]!, $offset: Int, $limit: Int) {
|
||||
jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) {
|
||||
aggregate {
|
||||
count(distinct: true)
|
||||
@@ -125,7 +120,7 @@ export const QUERY_PARTS_QUEUE = gql`
|
||||
}
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
order_by: { ro_number: desc }
|
||||
) {
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
@@ -142,7 +137,9 @@ export const QUERY_PARTS_QUEUE = gql`
|
||||
v_color
|
||||
vehicleid
|
||||
scheduled_in
|
||||
scheduled_completion
|
||||
id
|
||||
ins_co_nm
|
||||
clm_no
|
||||
ro_number
|
||||
status
|
||||
@@ -1865,6 +1862,7 @@ export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownerid
|
||||
plate_no
|
||||
plate_st
|
||||
v_vin
|
||||
@@ -2338,3 +2336,163 @@ export const MARK_JOB_AS_UNINVOICED = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||
query QUERY_JOB_CARD_DETAILS($id: uuid!) {
|
||||
jobs_by_pk(id: $id) {
|
||||
actual_completion
|
||||
actual_delivery
|
||||
actual_in
|
||||
alt_transport
|
||||
available_jobs {
|
||||
id
|
||||
}
|
||||
area_of_damage
|
||||
ca_gst_registrant
|
||||
cccontracts {
|
||||
agreementnumber
|
||||
courtesycar {
|
||||
id
|
||||
make
|
||||
model
|
||||
year
|
||||
plate
|
||||
fleetnumber
|
||||
}
|
||||
id
|
||||
scheduledreturn
|
||||
start
|
||||
status
|
||||
}
|
||||
clm_no
|
||||
clm_total
|
||||
comment
|
||||
date_estimated
|
||||
date_exported
|
||||
date_invoiced
|
||||
date_last_contacted
|
||||
date_next_contact
|
||||
date_open
|
||||
date_repairstarted
|
||||
date_scheduled
|
||||
ded_amt
|
||||
employee_body
|
||||
employee_body_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
employee_csr
|
||||
employee_csr_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
employee_prep
|
||||
employee_prep_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
employee_refinish
|
||||
employee_refinish_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
est_co_nm
|
||||
est_ct_fn
|
||||
est_ct_ln
|
||||
est_ea
|
||||
est_ph1
|
||||
id
|
||||
ins_co_nm
|
||||
ins_ct_fn
|
||||
ins_ct_ln
|
||||
ins_ea
|
||||
ins_ph1
|
||||
inproduction
|
||||
job_totals
|
||||
joblines(
|
||||
order_by: { line_no: asc }
|
||||
where: {
|
||||
part_type: {
|
||||
_in: [
|
||||
"PAN"
|
||||
"PAC"
|
||||
"PAR"
|
||||
"PAL"
|
||||
"PAA"
|
||||
"PAM"
|
||||
"PAP"
|
||||
"PAG"
|
||||
]
|
||||
}
|
||||
removed: { _eq: false }
|
||||
}
|
||||
) {
|
||||
act_price
|
||||
alt_partno
|
||||
db_ref
|
||||
id
|
||||
line_desc
|
||||
line_no
|
||||
location
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
oem_partno
|
||||
part_qty
|
||||
part_type
|
||||
prt_dsmk_m
|
||||
status
|
||||
}
|
||||
lbr_adjustments
|
||||
ownr_co_nm
|
||||
ownr_ea
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
owner {
|
||||
id
|
||||
allow_text_message
|
||||
preferred_contact
|
||||
tax_number
|
||||
}
|
||||
owner_owing
|
||||
plate_no
|
||||
plate_st
|
||||
po_number
|
||||
production_vars
|
||||
ro_number
|
||||
scheduled_completion
|
||||
scheduled_delivery
|
||||
scheduled_in
|
||||
special_coverage_policy
|
||||
status
|
||||
suspended
|
||||
updated_at
|
||||
vehicle {
|
||||
id
|
||||
jobs {
|
||||
id
|
||||
clm_no
|
||||
ro_number
|
||||
}
|
||||
notes
|
||||
plate_no
|
||||
v_color
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
v_model_yr
|
||||
}
|
||||
vehicleid
|
||||
v_color
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
v_model_yr
|
||||
v_vin
|
||||
voided
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -34,7 +34,12 @@ Sentry.init({
|
||||
maskAllText: false,
|
||||
blockAllMedia: true,
|
||||
}),
|
||||
new Sentry.BrowserTracing(),
|
||||
new Sentry.BrowserTracing({}),
|
||||
],
|
||||
tracePropagationTargets: [
|
||||
"api.imex.online",
|
||||
"api.test.imex.online",
|
||||
"db.imex.online",
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
@@ -1,88 +1,67 @@
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { Form, Layout, Typography, Button, Result } from "antd";
|
||||
import React, { useState } from "react";
|
||||
// import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Form, Layout, Result, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import ConfigFormComponents from "../../components/config-form-components/config-form-components.component";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
import { QUERY_SURVEY, COMPLETE_SURVEY } from "../../graphql/csi.queries";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormat } from "./../../utils/DateFormatter";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage);
|
||||
|
||||
export function CsiContainerPage({ currentUser }) {
|
||||
const { surveyId } = useParams();
|
||||
const [form] = Form.useForm();
|
||||
const [axiosResponse, setAxiosResponse] = useState(null);
|
||||
const [submitting, setSubmitting] = useState({
|
||||
loading: false,
|
||||
submitted: false,
|
||||
});
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY_SURVEY, {
|
||||
variables: { surveyId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [completeSurvey] = useMutation(COMPLETE_SURVEY);
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
if (error || !!!data.csi_by_pk)
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
status="error"
|
||||
title={t("csi.errors.notfoundtitle")}
|
||||
subTitle={t("csi.errors.notfoundsubtitle")}
|
||||
>
|
||||
{error ? (
|
||||
<div>ERROR: {error.graphQLErrors.map((e) => e.message)}</div>
|
||||
) : null}
|
||||
</Result>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setSubmitting({ ...submitting, loading: true });
|
||||
|
||||
const result = await completeSurvey({
|
||||
variables: {
|
||||
surveyId,
|
||||
survey: {
|
||||
response: values,
|
||||
valid: false,
|
||||
completedon: new Date(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
setSubmitting({ ...submitting, loading: false, submitted: true });
|
||||
} else {
|
||||
setSubmitting({
|
||||
...submitting,
|
||||
const getAxiosData = useCallback(async () => {
|
||||
try {
|
||||
try {
|
||||
window.$crisp.push(["do", "chat:hide"]);
|
||||
} catch {
|
||||
console.log("Unable to attach to crisp instance. ");
|
||||
}
|
||||
setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true }));
|
||||
const response = await axios.post("/csi/lookup", { surveyId });
|
||||
setSubmitting((prevSubmitting) => ({
|
||||
...prevSubmitting,
|
||||
loading: false,
|
||||
error: JSON.stringify(result.errors),
|
||||
}));
|
||||
setAxiosResponse(response.data);
|
||||
} catch (error) {
|
||||
console.error(`Something went wrong...: ${error.message}`);
|
||||
console.dir({
|
||||
stack: error?.stack,
|
||||
message: error?.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [setAxiosResponse, surveyId]);
|
||||
|
||||
const {
|
||||
relateddata: { bodyshop, job },
|
||||
csiquestion: { config: csiquestions },
|
||||
} = data.csi_by_pk;
|
||||
useEffect(() => {
|
||||
getAxiosData().catch((err) =>
|
||||
console.error(
|
||||
`Something went wrong fetching axios data: ${err.message || ""}`
|
||||
)
|
||||
);
|
||||
}, [getAxiosData]);
|
||||
|
||||
if (currentUser && currentUser.authorized)
|
||||
// Return if authorized
|
||||
if (currentUser && currentUser.authorized) {
|
||||
return (
|
||||
<Layout
|
||||
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
|
||||
@@ -94,85 +73,176 @@ export function CsiContainerPage({ currentUser }) {
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
|
||||
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
|
||||
<img src={bodyshop.logo_img_path.src} alt="Logo" />
|
||||
) : null}
|
||||
<div style={{ margin: "2em" }}>
|
||||
<strong>{bodyshop.shopname || ""}</strong>
|
||||
<div>{`${bodyshop.address1 || ""}`}</div>
|
||||
<div>{`${bodyshop.address2 || ""}`}</div>
|
||||
<div>{`${bodyshop.city || ""} ${bodyshop.state || ""} ${
|
||||
bodyshop.zip_post || ""
|
||||
}`}</div>
|
||||
if (submitting.loading) return <LoadingSpinner />;
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
try {
|
||||
setSubmitting({ ...submitting, loading: true, submitting: true });
|
||||
const result = await axios.post("/csi/submit", { surveyId, values });
|
||||
console.log("result", result);
|
||||
if (!!!result.errors && result.data.update_csi.affected_rows > 0) {
|
||||
setSubmitting({ ...submitting, loading: false, submitted: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Something went wrong...: ${error.message}`);
|
||||
console.dir({
|
||||
stack: error?.stack,
|
||||
message: error?.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!axiosResponse || axiosResponse.csi_by_pk === null) {
|
||||
// Do something here , this is where you would return a loading box or something
|
||||
return (
|
||||
<>
|
||||
<Layout style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Form>
|
||||
<Result
|
||||
status="error"
|
||||
title={t("csi.errors.notfoundtitle")}
|
||||
subTitle={t("csi.errors.notfoundsubtitle")}
|
||||
/>
|
||||
</Form>
|
||||
</Layout.Content>
|
||||
<Layout.Footer>
|
||||
{t("csi.labels.copyright")}{" "}
|
||||
{t("csi.fields.surveyid", { surveyId: surveyId })}
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const {
|
||||
relateddata: { bodyshop, job },
|
||||
csiquestion: { config: csiquestions },
|
||||
} = axiosResponse.csi_by_pk;
|
||||
|
||||
return (
|
||||
<Layout style={{ display: "flex", flexDirection: "column" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
|
||||
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
|
||||
<img
|
||||
src={bodyshop.logo_img_path.src}
|
||||
alt={bodyshop.shopname.concat(" Logo")}
|
||||
height={bodyshop.logo_img_path.height}
|
||||
width={bodyshop.logo_img_path.width}
|
||||
/>
|
||||
) : null}
|
||||
<div style={{ margin: "2em", verticalAlign: "middle" }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
{bodyshop.shopname || ""}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph style={{ margin: 0 }}>
|
||||
{`${bodyshop.address1 || ""}${bodyshop.address2 ? ", " : ""}${
|
||||
bodyshop.address2 || ""
|
||||
}`.trim()}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph style={{ margin: 0 }}>
|
||||
{`${bodyshop.city || ""}${
|
||||
bodyshop.city && bodyshop.state ? ", " : ""
|
||||
}${bodyshop.state || ""} ${bodyshop.zip_post || ""}`.trim()}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
|
||||
<strong>
|
||||
{t("csi.labels.greeting", {
|
||||
name: job.ownr_co_nm || job.ownr_fn || "",
|
||||
})}
|
||||
</strong>
|
||||
<Typography.Paragraph>
|
||||
{t("csi.labels.intro", { shopname: bodyshop.shopname || "" })}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
|
||||
<strong>{`Hi ${job.ownr_co_nm || job.ownr_fn || ""}!`}</strong>
|
||||
<Typography.Paragraph>
|
||||
{`At ${
|
||||
bodyshop.shopname || ""
|
||||
}, we value your feedback. We would love to
|
||||
hear what you have to say. Please fill out the form below.`}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
{submitting.error ? (
|
||||
<AlertComponent message={submitting.error} type="error" />
|
||||
) : null}
|
||||
{submitting.error ? (
|
||||
<AlertComponent message={submitting.error} type="error" />
|
||||
) : null}
|
||||
|
||||
{submitting.submitted ? (
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
status="success"
|
||||
title={t("csi.successes.submitted")}
|
||||
subTitle={t("csi.successes.submittedsub")}
|
||||
/>
|
||||
</Layout.Content>
|
||||
) : (
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<ConfigFormComponents componentList={csiquestions} />
|
||||
<Button
|
||||
loading={submitting.loading}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
>
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
</Form>
|
||||
</Layout.Content>
|
||||
)}
|
||||
|
||||
<Layout.Footer>
|
||||
{`Copyright ImEX.Online. Survey ID: ${surveyId}`}
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
);
|
||||
{submitting.submitted ? (
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
status="success"
|
||||
title={t("csi.successes.submitted")}
|
||||
subTitle={t("csi.successes.submittedsub")}
|
||||
/>
|
||||
</Layout.Content>
|
||||
) : (
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
{axiosResponse.csi_by_pk.valid ? (
|
||||
<>
|
||||
<ConfigFormComponents componentList={csiquestions} />
|
||||
<Button
|
||||
loading={submitting.loading}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{ float: "right" }}
|
||||
>
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Result
|
||||
title={t("csi.errors.surveycompletetitle")}
|
||||
status="warning"
|
||||
subTitle={t("csi.errors.surveycompletesubtitle", {
|
||||
date: DateTimeFormat(axiosResponse.csi_by_pk.completedon),
|
||||
})}
|
||||
/>
|
||||
<Typography.Paragraph
|
||||
type="secondary"
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{t("csi.successes.submittedsub")}
|
||||
</Typography.Paragraph>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Layout.Content>
|
||||
)}
|
||||
<Layout.Footer>
|
||||
{t("csi.labels.copyright")}{" "}
|
||||
{t("csi.fields.surveyid", { surveyId: surveyId })}
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,12 @@ import { OwnerNameDisplayFunction } from "../../components/owner-name-display/ow
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
|
||||
import {
|
||||
insertAuditTrail,
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -38,6 +40,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||
@@ -46,7 +50,7 @@ export const socket = SocketIO(
|
||||
process.env.NODE_ENV === "production"
|
||||
? process.env.REACT_APP_AXIOS_BASE_API_URL
|
||||
: window.location.origin,
|
||||
// "http://localhost:4000", // for dev testing,
|
||||
// "http://localhost:4000", // for dev testing,
|
||||
{
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
@@ -57,7 +61,12 @@ export const socket = SocketIO(
|
||||
}
|
||||
);
|
||||
|
||||
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
export function DmsContainer({
|
||||
bodyshop,
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [logLevel, setLogLevel] = useState("DEBUG");
|
||||
const history = useHistory();
|
||||
@@ -115,6 +124,11 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
notification.success({
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: payload,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
history.push("/manage/accounting/receivables");
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import AlertComponent from "../../components/alert/alert.component";
|
||||
import { QUERY_EXPORT_LOG_PAGINATED } from "../../graphql/accounting.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "./../../utils/sorters";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -34,11 +35,43 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
limit: pageLimit,
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
...(sortcolumn === "ro_number"
|
||||
? {
|
||||
job: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
}
|
||||
: sortcolumn === "invoice_number"
|
||||
? {
|
||||
bill: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
}
|
||||
: sortcolumn === "paymentnum"
|
||||
? {
|
||||
payment: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
}
|
||||
: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -68,6 +101,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("general.labels.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
||||
sortOrder: sortcolumn === "created_at" && sortorder,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
),
|
||||
@@ -81,7 +116,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
render: (text, record) =>
|
||||
record.job && (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
@@ -93,6 +129,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("bills.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
sortOrder: sortcolumn === "invoice_number" && sortorder,
|
||||
render: (text, record) =>
|
||||
record.bill && (
|
||||
<Link to={"/manage/bills?billid=" + (record.bill && record.bill.id)}>
|
||||
@@ -104,6 +142,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("payments.fields.paymentnum"),
|
||||
dataIndex: "paymentnum",
|
||||
key: "paymentnum",
|
||||
sorter: (a, b) => alphaSort(a.paymentnum, b.paymentnum),
|
||||
sortOrder: sortcolumn === "paymentnum" && sortorder,
|
||||
render: (text, record) =>
|
||||
record.payment && (
|
||||
<Link
|
||||
@@ -120,6 +160,13 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("general.labels.successful"),
|
||||
dataIndex: "successful",
|
||||
key: "successful",
|
||||
sorter: (a, b) => Number(a.successful) - Number(b.successful),
|
||||
sortOrder: sortcolumn === "successful" && sortorder,
|
||||
filters: [
|
||||
{ text: "True", value: true },
|
||||
{ text: "False", value: false },
|
||||
],
|
||||
onFilter: (value, record) => record.successful === value,
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.successful} />
|
||||
),
|
||||
|
||||
@@ -47,11 +47,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, }) {
|
||||
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const client = useApolloClient();
|
||||
@@ -121,6 +121,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, })
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobinvoiced(),
|
||||
type: "jobinvoiced",
|
||||
});
|
||||
// history.push(`/manage/jobs/${job.id}`);
|
||||
} else {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
|
||||
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
|
||||
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
|
||||
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
|
||||
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
|
||||
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
|
||||
@@ -54,7 +55,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import { DateTimeFormat } from "./../../utils/DateFormatter";
|
||||
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -63,8 +63,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPrintCenterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export function JobsDetailPage({
|
||||
bodyshop,
|
||||
@@ -177,6 +177,7 @@ export function JobsDetailPage({
|
||||
? DateTimeFormat(changedAuditFields[key])
|
||||
: changedAuditFields[key]
|
||||
),
|
||||
type: "jobfieldchange",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -334,15 +335,23 @@ export function JobsDetailPage({
|
||||
>
|
||||
<JobsDetailLaborContainer job={job} jobId={job.id} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
forceRender
|
||||
tab={<span><BarsOutlined />{t('menus.jobsdetail.lifecycle')}</span>}
|
||||
key="lifecycle"
|
||||
>
|
||||
<JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses}/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
forceRender
|
||||
tab={
|
||||
<span>
|
||||
<BarsOutlined />
|
||||
{t("menus.jobsdetail.lifecycle")}
|
||||
</span>
|
||||
}
|
||||
key="lifecycle"
|
||||
>
|
||||
<JobLifecycleComponent
|
||||
job={job}
|
||||
statuses={bodyshop.md_ro_statuses}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane
|
||||
<Tabs.TabPane
|
||||
forceRender
|
||||
tab={
|
||||
<span>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import PartsQueueDetailCard from "../../components/parts-queue-card/parts-queue-card.component";
|
||||
import PartsQueueList from "../../components/parts-queue-list/parts-queue.list.component";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import PartsQueuePage from "./parts-queue.page.component";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
@@ -26,7 +27,8 @@ export function PartsQueuePageContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
|
||||
return (
|
||||
<RbacWrapper action="jobs:partsqueue">
|
||||
<PartsQueuePage />
|
||||
<PartsQueueList />
|
||||
<PartsQueueDetailCard />
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { Row, Col } from "antd";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
import { Col, Row } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import CsiResponseFormContainer from "../../components/csi-response-form/csi-response-form.container";
|
||||
import CsiResponseListPaginated from "../../components/csi-response-list-paginated/csi-response-list-paginated.component";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import { QUERY_CSI_RESPONSE_PAGINATED } from "../../graphql/csi.queries";
|
||||
import {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
@@ -33,28 +31,11 @@ export function ShopCsiContainer({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { page, sortcolumn, sortorder } = searchParams;
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(
|
||||
QUERY_CSI_RESPONSE_PAGINATED,
|
||||
{
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
//search: search || "",
|
||||
offset: page ? (page - 1) * pageLimit : 0,
|
||||
limit: pageLimit,
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "completedon"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc_nulls_last"
|
||||
: "asc"
|
||||
: "desc_nulls_last",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -73,12 +54,7 @@ export function ShopCsiContainer({
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<RbacWrapper
|
||||
action="csi:page"
|
||||
// noauth={
|
||||
// <AlertComponent message="You don't have acess to see this screen." />
|
||||
// }
|
||||
>
|
||||
<RbacWrapper action="csi:page">
|
||||
<Row gutter={16}>
|
||||
<Col span={10}>
|
||||
<CsiResponseListPaginated
|
||||
|
||||
@@ -54,9 +54,9 @@ export const setOnline = (isOnline) => ({
|
||||
payload: isOnline,
|
||||
});
|
||||
|
||||
export const insertAuditTrail = ({ jobid, billid, operation }) => ({
|
||||
export const insertAuditTrail = ({ jobid, billid, operation, type }) => ({
|
||||
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
|
||||
payload: { jobid, billid, operation },
|
||||
payload: { jobid, billid, operation, type },
|
||||
});
|
||||
export const setProblemJobs = (problemJobs) => ({
|
||||
type: ApplicationActionTypes.SET_PROBLEM_JOBS,
|
||||
|
||||
@@ -266,7 +266,7 @@ export function* onInsertAuditTrail() {
|
||||
}
|
||||
|
||||
export function* insertAuditTrailSaga({
|
||||
payload: { jobid, billid, operation },
|
||||
payload: { jobid, billid, operation, type },
|
||||
}) {
|
||||
const state = yield select();
|
||||
const bodyshop = state.user.bodyshop;
|
||||
@@ -278,6 +278,7 @@ export function* insertAuditTrailSaga({
|
||||
jobid,
|
||||
billid,
|
||||
operation,
|
||||
type,
|
||||
useremail: currentUser.email,
|
||||
},
|
||||
};
|
||||
@@ -289,7 +290,7 @@ export function* insertAuditTrailSaga({
|
||||
fields: {
|
||||
audit_trail(existingAuditTrail, { readField }) {
|
||||
const newAuditTrail = cache.writeQuery({
|
||||
data: data.insert_audit_trail_one,
|
||||
data: data,
|
||||
query: INSERT_AUDIT_TRAIL,
|
||||
variables,
|
||||
});
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"alerttoggle": "Alert Toggle set to {{status}}",
|
||||
"appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.",
|
||||
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
|
||||
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
|
||||
"billposted": "Bill with invoice number {{invoice_number}} posted.",
|
||||
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
|
||||
"failedpayment": "Failed payment",
|
||||
@@ -114,6 +115,7 @@
|
||||
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
|
||||
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
|
||||
"jobconverted": "Job converted and assigned number {{ro_number}}.",
|
||||
"jobexported": "Job has been exported.",
|
||||
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
|
||||
"jobimported": "Job imported.",
|
||||
"jobinproductionchange": "Job production status set to {{inproduction}}",
|
||||
@@ -126,7 +128,9 @@
|
||||
"jobspartsorder": "Parts order {{order_number}} added to Job.",
|
||||
"jobspartsreturn": "Parts return {{order_number}} added to Job.",
|
||||
"jobstatuschange": "Job status changed to {{status}}.",
|
||||
"jobsupplement": "Job supplement imported."
|
||||
"jobsupplement": "Job supplement imported.",
|
||||
"jobsuspend": "Suspend Toggle set to {{status}}",
|
||||
"jobvoid": "Job has been voided."
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
@@ -747,6 +751,7 @@
|
||||
"driverinformation": "Driver's Information",
|
||||
"findcontract": "Find Contract",
|
||||
"findermodal": "Contract Finder",
|
||||
"insuranceexpired": "The courtesy car insurance expires before the car is expected to return.",
|
||||
"noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.",
|
||||
"populatefromjob": "Populate from Job",
|
||||
"rates": "Contract Rates",
|
||||
@@ -838,20 +843,27 @@
|
||||
"creating": "Error creating survey {{message}}",
|
||||
"notconfigured": "You do not have any current CSI Question Sets configured.",
|
||||
"notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.",
|
||||
"notfoundtitle": "No survey found."
|
||||
"notfoundtitle": "No survey found.",
|
||||
"surveycompletetitle": "Survey previously completed",
|
||||
"surveycompletesubtitle": "This survey was already completed on {{date}}."
|
||||
},
|
||||
"fields": {
|
||||
"completedon": "Completed On",
|
||||
"created_at": "Created At"
|
||||
"created_at": "Created At",
|
||||
"surveyid": "Survey ID {{surveyId}}",
|
||||
"validuntil": "Valid Until"
|
||||
},
|
||||
"labels": {
|
||||
"nologgedinuser": "Please log out of ImEX Online",
|
||||
"nologgedinuser_sub": "Users of ImEX Online cannot complete CSI surveys while logged in. Please log out and try again.",
|
||||
"nologgedinuser": "Please log out of $t(titles.app)",
|
||||
"nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.",
|
||||
"noneselected": "No response selected.",
|
||||
"title": "Customer Satisfaction Survey"
|
||||
"title": "Customer Satisfaction Survey",
|
||||
"greeting": "Hi {{name}}!",
|
||||
"intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.",
|
||||
"copyright": "Copyright © $t(titles.app). All Rights Reserved."
|
||||
},
|
||||
"successes": {
|
||||
"created": "CSI created successfully. ",
|
||||
"created": "CSI created successfully.",
|
||||
"submitted": "Your responses have been submitted successfully.",
|
||||
"submittedsub": "Your input is highly appreciated."
|
||||
}
|
||||
@@ -867,6 +879,7 @@
|
||||
"labels": {
|
||||
"bodyhrs": "Body Hrs",
|
||||
"dollarsinproduction": "Dollars in Production",
|
||||
"phone": "Phone",
|
||||
"prodhrs": "Production Hrs",
|
||||
"refhrs": "Refinish Hrs"
|
||||
},
|
||||
@@ -882,8 +895,10 @@
|
||||
"productiondollars": "Total Dollars in Production",
|
||||
"productionhours": "Total Hours in Production",
|
||||
"projectedmonthlysales": "Projected Monthly Sales",
|
||||
"scheduledintoday": "Sheduled In Today: {{date}}",
|
||||
"scheduledouttoday": "Sheduled Out Today: {{date}}"
|
||||
"scheduledindate": "Sheduled In Today: {{date}}",
|
||||
"scheduledintoday": "Sheduled In Today",
|
||||
"scheduledoutdate": "Sheduled Out Today: {{date}}",
|
||||
"scheduledouttoday": "Sheduled Out Today"
|
||||
}
|
||||
},
|
||||
"dms": {
|
||||
@@ -1725,6 +1740,7 @@
|
||||
"ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ",
|
||||
"calc_repair_days": "Calculated Repair Days",
|
||||
"calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).",
|
||||
"calc_scheuled_completion": "Calculate Scheduled Completion",
|
||||
"cards": {
|
||||
"customer": "Customer Information",
|
||||
"damage": "Area of Damage",
|
||||
@@ -1822,6 +1838,7 @@
|
||||
"override_header": "Override estimate header on import?",
|
||||
"ownerassociation": "Owner Association",
|
||||
"parts": "Parts",
|
||||
"parts_lines": "Parts Lines",
|
||||
"parts_received": "Parts Rec.",
|
||||
"parts_tax_rates": "Parts Tax rates",
|
||||
"partsfilter": "Parts Only",
|
||||
@@ -1883,6 +1900,7 @@
|
||||
"total_sales": "Total Sales",
|
||||
"totals": "Totals",
|
||||
"unvoidnote": "This Job was unvoided.",
|
||||
"update_scheduled_completion": "Update Scheduled Completion?",
|
||||
"vehicle_info": "Vehicle",
|
||||
"vehicleassociation": "Vehicle Association",
|
||||
"viewallocations": "View Allocations",
|
||||
@@ -2564,6 +2582,18 @@
|
||||
"generate": "Generate"
|
||||
},
|
||||
"labels": {
|
||||
"advanced_filters": "Advanced Filters and Sorters",
|
||||
"advanced_filters_show": "Show",
|
||||
"advanced_filters_hide": "Hide",
|
||||
"advanced_filters_filters": "Filters",
|
||||
"advanced_filters_sorters": "Sorters",
|
||||
"advanced_filters_filter_field": "Field",
|
||||
"advanced_filters_sorter_field": "Field",
|
||||
"advanced_filters_true": "True",
|
||||
"advanced_filters_false": "False",
|
||||
"advanced_filters_sorter_direction": "Direction",
|
||||
"advanced_filters_filter_operator": "Operator",
|
||||
"advanced_filters_filter_value": "Value",
|
||||
"dates": "Dates",
|
||||
"employee": "Employee",
|
||||
"filterson": "Filters on {{object}}: {{field}}",
|
||||
@@ -2734,6 +2764,7 @@
|
||||
"allemployeetimetickets": "All Employee Time Tickets",
|
||||
"asoftodaytarget": "As of Today",
|
||||
"body": "Body",
|
||||
"bodyabbrev": "B",
|
||||
"bodycharttitle": "Body Targets vs Actual",
|
||||
"calendarperiod": "Periods based on calendar weeks/months.",
|
||||
"combinedcharttitle": "Combined Targets vs Actual",
|
||||
@@ -2750,6 +2781,7 @@
|
||||
"productivestatistics": "Productive Hours Statistics",
|
||||
"productivetimeticketsoverdate": "Productive Hours over Selected Dates",
|
||||
"refinish": "Refinish",
|
||||
"refinishabbrev": "R",
|
||||
"refinishcharttitle": "Refinish Targets vs Actual",
|
||||
"targets": "Targets",
|
||||
"thismonth": "This Month",
|
||||
@@ -2757,6 +2789,7 @@
|
||||
"timetickets": "Time Tickets",
|
||||
"timeticketsemployee": "Time Tickets by Employee",
|
||||
"todateactual": "Actual (MTD)",
|
||||
"total": "Total",
|
||||
"totalhrs": "Total Hours",
|
||||
"totaloverperiod": "Total over Selected Dates",
|
||||
"weeklyactual": "Actual (W)",
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"alerttoggle": "",
|
||||
"appointmentcancel": "",
|
||||
"appointmentinsert": "",
|
||||
"billdeleted": "",
|
||||
"billposted": "",
|
||||
"billupdated": "",
|
||||
"failedpayment": "",
|
||||
@@ -114,6 +115,7 @@
|
||||
"jobassignmentremoved": "",
|
||||
"jobchecklist": "",
|
||||
"jobconverted": "",
|
||||
"jobexported": "",
|
||||
"jobfieldchanged": "",
|
||||
"jobimported": "",
|
||||
"jobinproductionchange": "",
|
||||
@@ -126,7 +128,9 @@
|
||||
"jobspartsorder": "",
|
||||
"jobspartsreturn": "",
|
||||
"jobstatuschange": "",
|
||||
"jobsupplement": ""
|
||||
"jobsupplement": "",
|
||||
"jobsuspend": "",
|
||||
"jobvoid": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
@@ -747,6 +751,7 @@
|
||||
"driverinformation": "",
|
||||
"findcontract": "",
|
||||
"findermodal": "",
|
||||
"insuranceexpired": "",
|
||||
"noteconvertedfrom": "",
|
||||
"populatefromjob": "",
|
||||
"rates": "",
|
||||
@@ -838,17 +843,24 @@
|
||||
"creating": "",
|
||||
"notconfigured": "",
|
||||
"notfoundsubtitle": "",
|
||||
"notfoundtitle": ""
|
||||
"notfoundtitle": "",
|
||||
"surveycompletetitle": "",
|
||||
"surveycompletesubtitle": ""
|
||||
},
|
||||
"fields": {
|
||||
"completedon": "",
|
||||
"created_at": ""
|
||||
"created_at": "",
|
||||
"surveyid": "",
|
||||
"validuntil": ""
|
||||
},
|
||||
"labels": {
|
||||
"nologgedinuser": "",
|
||||
"nologgedinuser_sub": "",
|
||||
"noneselected": "",
|
||||
"title": ""
|
||||
"title": "",
|
||||
"greeting": "",
|
||||
"intro": "",
|
||||
"copyright": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -867,6 +879,7 @@
|
||||
"labels": {
|
||||
"bodyhrs": "",
|
||||
"dollarsinproduction": "",
|
||||
"phone": "",
|
||||
"prodhrs": "",
|
||||
"refhrs": ""
|
||||
},
|
||||
@@ -882,7 +895,9 @@
|
||||
"productiondollars": "",
|
||||
"productionhours": "",
|
||||
"projectedmonthlysales": "",
|
||||
"scheduledindate": "",
|
||||
"scheduledintoday": "",
|
||||
"scheduledoutdate": "",
|
||||
"scheduledouttoday": ""
|
||||
}
|
||||
},
|
||||
@@ -1725,6 +1740,7 @@
|
||||
"ca_gst_all_if_null": "",
|
||||
"calc_repair_days": "",
|
||||
"calc_repair_days_tt": "",
|
||||
"calc_scheuled_completion": "",
|
||||
"cards": {
|
||||
"customer": "Información al cliente",
|
||||
"damage": "Área de Daño",
|
||||
@@ -1822,6 +1838,7 @@
|
||||
"override_header": "¿Anular encabezado estimado al importar?",
|
||||
"ownerassociation": "",
|
||||
"parts": "Partes",
|
||||
"parts_lines": "",
|
||||
"parts_received": "",
|
||||
"parts_tax_rates": "",
|
||||
"partsfilter": "",
|
||||
@@ -1883,6 +1900,7 @@
|
||||
"total_sales": "",
|
||||
"totals": "",
|
||||
"unvoidnote": "",
|
||||
"update_scheduled_completion": "",
|
||||
"vehicle_info": "Vehículo",
|
||||
"vehicleassociation": "",
|
||||
"viewallocations": "",
|
||||
@@ -2564,6 +2582,18 @@
|
||||
"generate": ""
|
||||
},
|
||||
"labels": {
|
||||
"advanced_filters": "",
|
||||
"advanced_filters_show": "",
|
||||
"advanced_filters_hide": "",
|
||||
"advanced_filters_filters": "",
|
||||
"advanced_filters_sorters": "",
|
||||
"advanced_filters_filter_field": "",
|
||||
"advanced_filters_sorter_field": "",
|
||||
"advanced_filters_true": "",
|
||||
"advanced_filters_false": "",
|
||||
"advanced_filters_sorter_direction": "",
|
||||
"advanced_filters_filter_operator": "",
|
||||
"advanced_filters_filter_value": "",
|
||||
"dates": "",
|
||||
"employee": "",
|
||||
"filterson": "",
|
||||
@@ -2734,6 +2764,7 @@
|
||||
"allemployeetimetickets": "",
|
||||
"asoftodaytarget": "",
|
||||
"body": "",
|
||||
"bodyabbrev": "",
|
||||
"bodycharttitle": "",
|
||||
"calendarperiod": "",
|
||||
"combinedcharttitle": "",
|
||||
@@ -2750,6 +2781,7 @@
|
||||
"productivestatistics": "",
|
||||
"productivetimeticketsoverdate": "",
|
||||
"refinish": "",
|
||||
"refinishabbrev": "",
|
||||
"refinishcharttitle": "",
|
||||
"targets": "",
|
||||
"thismonth": "",
|
||||
@@ -2757,6 +2789,7 @@
|
||||
"timetickets": "",
|
||||
"timeticketsemployee": "",
|
||||
"todateactual": "",
|
||||
"total": "",
|
||||
"totalhrs": "",
|
||||
"totaloverperiod": "",
|
||||
"weeklyactual": "",
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"alerttoggle": "",
|
||||
"appointmentcancel": "",
|
||||
"appointmentinsert": "",
|
||||
"billdeleted": "",
|
||||
"billposted": "",
|
||||
"billupdated": "",
|
||||
"failedpayment": "",
|
||||
@@ -114,6 +115,7 @@
|
||||
"jobassignmentremoved": "",
|
||||
"jobchecklist": "",
|
||||
"jobconverted": "",
|
||||
"jobexported": "",
|
||||
"jobfieldchanged": "",
|
||||
"jobimported": "",
|
||||
"jobinproductionchange": "",
|
||||
@@ -126,7 +128,9 @@
|
||||
"jobspartsorder": "",
|
||||
"jobspartsreturn": "",
|
||||
"jobstatuschange": "",
|
||||
"jobsupplement": ""
|
||||
"jobsupplement": "",
|
||||
"jobsuspend": "",
|
||||
"jobvoid": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
@@ -747,6 +751,7 @@
|
||||
"driverinformation": "",
|
||||
"findcontract": "",
|
||||
"findermodal": "",
|
||||
"insuranceexpired": "",
|
||||
"noteconvertedfrom": "",
|
||||
"populatefromjob": "",
|
||||
"rates": "",
|
||||
@@ -838,17 +843,24 @@
|
||||
"creating": "",
|
||||
"notconfigured": "",
|
||||
"notfoundsubtitle": "",
|
||||
"notfoundtitle": ""
|
||||
"notfoundtitle": "",
|
||||
"surveycompletetitle": "",
|
||||
"surveycompletesubtitle": ""
|
||||
},
|
||||
"fields": {
|
||||
"completedon": "",
|
||||
"created_at": ""
|
||||
"created_at": "",
|
||||
"surveyid": "",
|
||||
"validuntil": ""
|
||||
},
|
||||
"labels": {
|
||||
"nologgedinuser": "",
|
||||
"nologgedinuser_sub": "",
|
||||
"noneselected": "",
|
||||
"title": ""
|
||||
"title": "",
|
||||
"greeting": "",
|
||||
"intro": "",
|
||||
"copyright": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -867,6 +879,7 @@
|
||||
"labels": {
|
||||
"bodyhrs": "",
|
||||
"dollarsinproduction": "",
|
||||
"phone": "",
|
||||
"prodhrs": "",
|
||||
"refhrs": ""
|
||||
},
|
||||
@@ -882,7 +895,9 @@
|
||||
"productiondollars": "",
|
||||
"productionhours": "",
|
||||
"projectedmonthlysales": "",
|
||||
"scheduledindate": "",
|
||||
"scheduledintoday": "",
|
||||
"scheduledoutdate": "",
|
||||
"scheduledouttoday": ""
|
||||
}
|
||||
},
|
||||
@@ -1725,6 +1740,7 @@
|
||||
"ca_gst_all_if_null": "",
|
||||
"calc_repair_days": "",
|
||||
"calc_repair_days_tt": "",
|
||||
"calc_scheuled_completion": "",
|
||||
"cards": {
|
||||
"customer": "Informations client",
|
||||
"damage": "Zone de dommages",
|
||||
@@ -1822,6 +1838,7 @@
|
||||
"override_header": "Remplacer l'en-tête d'estimation à l'importation?",
|
||||
"ownerassociation": "",
|
||||
"parts": "les pièces",
|
||||
"parts_lines": "",
|
||||
"parts_received": "",
|
||||
"parts_tax_rates": "",
|
||||
"partsfilter": "",
|
||||
@@ -1883,6 +1900,7 @@
|
||||
"total_sales": "",
|
||||
"totals": "",
|
||||
"unvoidnote": "",
|
||||
"update_scheduled_completion": "",
|
||||
"vehicle_info": "Véhicule",
|
||||
"vehicleassociation": "",
|
||||
"viewallocations": "",
|
||||
@@ -2564,6 +2582,18 @@
|
||||
"generate": ""
|
||||
},
|
||||
"labels": {
|
||||
"advanced_filters": "",
|
||||
"advanced_filters_show": "",
|
||||
"advanced_filters_hide": "",
|
||||
"advanced_filters_filters": "",
|
||||
"advanced_filters_sorters": "",
|
||||
"advanced_filters_filter_field": "",
|
||||
"advanced_filters_sorter_field": "",
|
||||
"advanced_filters_true": "",
|
||||
"advanced_filters_false": "",
|
||||
"advanced_filters_sorter_direction": "",
|
||||
"advanced_filters_filter_operator": "",
|
||||
"advanced_filters_filter_value": "",
|
||||
"dates": "",
|
||||
"employee": "",
|
||||
"filterson": "",
|
||||
@@ -2734,6 +2764,7 @@
|
||||
"allemployeetimetickets": "",
|
||||
"asoftodaytarget": "",
|
||||
"body": "",
|
||||
"bodyabbrev": "",
|
||||
"bodycharttitle": "",
|
||||
"calendarperiod": "",
|
||||
"combinedcharttitle": "",
|
||||
@@ -2750,6 +2781,7 @@
|
||||
"productivestatistics": "",
|
||||
"productivetimeticketsoverdate": "",
|
||||
"refinish": "",
|
||||
"refinishabbrev": "",
|
||||
"refinishcharttitle": "",
|
||||
"targets": "",
|
||||
"thismonth": "",
|
||||
@@ -2757,6 +2789,7 @@
|
||||
"timetickets": "",
|
||||
"timeticketsemployee": "",
|
||||
"todateactual": "",
|
||||
"total": "",
|
||||
"totalhrs": "",
|
||||
"totaloverperiod": "",
|
||||
"weeklyactual": "",
|
||||
|
||||
@@ -20,6 +20,8 @@ const AuditTrailMapping = {
|
||||
i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
|
||||
appointmentinsert: (start) =>
|
||||
i18n.t("audit_trail.messages.appointmentinsert", { start }),
|
||||
billdeleted: (invoice_number) =>
|
||||
i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
|
||||
billposted: (invoice_number) =>
|
||||
i18n.t("audit_trail.messages.billposted", { invoice_number }),
|
||||
billupdated: (invoice_number) =>
|
||||
@@ -33,6 +35,7 @@ const AuditTrailMapping = {
|
||||
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
||||
jobconverted: (ro_number) =>
|
||||
i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
||||
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
||||
jobfieldchange: (field, value) =>
|
||||
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
|
||||
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
|
||||
@@ -51,6 +54,8 @@ const AuditTrailMapping = {
|
||||
jobstatuschange: (status) =>
|
||||
i18n.t("audit_trail.messages.jobstatuschange", { status }),
|
||||
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
|
||||
jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }),
|
||||
jobvoid: () => i18n.t("audit_trail.messages.jobvoid"),
|
||||
};
|
||||
|
||||
export default AuditTrailMapping;
|
||||
|
||||
@@ -24,4 +24,13 @@ const range = {
|
||||
],
|
||||
"Last 90 Days": [moment().add(-90, "days"), moment()],
|
||||
};
|
||||
|
||||
// We are development, lets get crazy
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
range["Last year"] = [
|
||||
moment().subtract(1, "year"),
|
||||
moment(),
|
||||
];
|
||||
}
|
||||
|
||||
export default range;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { gql } from "@apollo/client";
|
||||
import {gql} from "@apollo/client";
|
||||
import jsreport from "@jsreport/browser-client";
|
||||
import { notification } from "antd";
|
||||
import {notification} from "antd";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
import { setEmailOptions } from "../redux/email/email.actions";
|
||||
import { store } from "../redux/store";
|
||||
import {auth} from "../firebase/firebase.utils";
|
||||
import {setEmailOptions} from "../redux/email/email.actions";
|
||||
import {store} from "../redux/store";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import cleanAxios from "./CleanAxios";
|
||||
import { TemplateList } from "./TemplateConstants";
|
||||
import {TemplateList} from "./TemplateConstants";
|
||||
import {generateTemplate} from "./graphQLmodifier";
|
||||
|
||||
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
|
||||
|
||||
jsreport.serverUrl = server;
|
||||
@@ -16,11 +18,11 @@ jsreport.serverUrl = server;
|
||||
const Templates = TemplateList();
|
||||
|
||||
export default async function RenderTemplate(
|
||||
templateObject,
|
||||
bodyshop,
|
||||
renderAsHtml = false,
|
||||
renderAsExcel = false,
|
||||
renderAsText = false
|
||||
templateObject,
|
||||
bodyshop,
|
||||
renderAsHtml = false,
|
||||
renderAsExcel = false,
|
||||
renderAsText = false
|
||||
) {
|
||||
if (window.jsr3) {
|
||||
jsreport.serverUrl = "https://reports3.test.imex.online/";
|
||||
@@ -30,41 +32,41 @@ export default async function RenderTemplate(
|
||||
jsreport.headers["Authorization"] = jsrAuth;
|
||||
|
||||
//Query assets that match the template name. Must be in format <<templateName>>.query
|
||||
let { contextData, useShopSpecificTemplate } = await fetchContextData(
|
||||
templateObject,
|
||||
jsrAuth
|
||||
let {contextData, useShopSpecificTemplate} = await fetchContextData(
|
||||
templateObject,
|
||||
jsrAuth
|
||||
);
|
||||
|
||||
const { ignoreCustomMargins } = Templates[templateObject.name];
|
||||
const {ignoreCustomMargins} = Templates[templateObject.name];
|
||||
|
||||
let reportRequest = {
|
||||
template: {
|
||||
name: useShopSpecificTemplate
|
||||
? `/${bodyshop.imexshopid}/${templateObject.name}`
|
||||
: `/${templateObject.name}`,
|
||||
? `/${bodyshop.imexshopid}/${templateObject.name}`
|
||||
: `/${templateObject.name}`,
|
||||
...(renderAsHtml
|
||||
? {}
|
||||
: {
|
||||
? {}
|
||||
: {
|
||||
recipe: "chrome-pdf",
|
||||
...(!ignoreCustomMargins && {
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
|
||||
...(renderAsText ? { recipe: "text" } : {}),
|
||||
...(renderAsExcel ? {recipe: "html-to-xlsx"} : {}),
|
||||
...(renderAsText ? {recipe: "text"} : {}),
|
||||
},
|
||||
data: {
|
||||
...contextData,
|
||||
@@ -73,7 +75,10 @@ export default async function RenderTemplate(
|
||||
headerpath: `/${bodyshop.imexshopid}/header.html`,
|
||||
footerpath: `/${bodyshop.imexshopid}/footer.html`,
|
||||
bodyshop: bodyshop,
|
||||
offset: bodyshop.timezone, //moment().utcOffset(),
|
||||
filters: templateObject?.filters,
|
||||
sorters: templateObject?.sorters,
|
||||
offset: bodyshop.timezone, //dayjs().utcOffset(),
|
||||
defaultSorters: templateObject?.defaultSorters,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -82,8 +87,8 @@ export default async function RenderTemplate(
|
||||
|
||||
if (!renderAsHtml) {
|
||||
render.download(
|
||||
(Templates[templateObject.name] &&
|
||||
Templates[templateObject.name].title) ||
|
||||
(Templates[templateObject.name] &&
|
||||
Templates[templateObject.name].title) ||
|
||||
""
|
||||
);
|
||||
} else {
|
||||
@@ -97,17 +102,17 @@ export default async function RenderTemplate(
|
||||
...(!ignoreCustomMargins && {
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -121,21 +126,21 @@ export default async function RenderTemplate(
|
||||
resolve({
|
||||
pdf,
|
||||
filename:
|
||||
Templates[templateObject.name] &&
|
||||
Templates[templateObject.name].title,
|
||||
Templates[templateObject.name] &&
|
||||
Templates[templateObject.name].title,
|
||||
html,
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification["error"]({ message: JSON.stringify(error) });
|
||||
notification["error"]({message: JSON.stringify(error)});
|
||||
}
|
||||
}
|
||||
|
||||
export async function RenderTemplates(
|
||||
templateObjects,
|
||||
bodyshop,
|
||||
renderAsHtml = false
|
||||
templateObjects,
|
||||
bodyshop,
|
||||
renderAsHtml = false
|
||||
) {
|
||||
//Query assets that match the template name. Must be in format <<templateName>>.query
|
||||
let unsortedTemplatesAndData = [];
|
||||
@@ -145,17 +150,17 @@ export async function RenderTemplates(
|
||||
|
||||
templateObjects.forEach((template) => {
|
||||
proms.push(
|
||||
(async () => {
|
||||
let { contextData, useShopSpecificTemplate } = await fetchContextData(
|
||||
template,
|
||||
jsrAuth
|
||||
);
|
||||
unsortedTemplatesAndData.push({
|
||||
templateObject: template,
|
||||
contextData,
|
||||
useShopSpecificTemplate,
|
||||
});
|
||||
})()
|
||||
(async () => {
|
||||
let {contextData, useShopSpecificTemplate} = await fetchContextData(
|
||||
template,
|
||||
jsrAuth
|
||||
);
|
||||
unsortedTemplatesAndData.push({
|
||||
templateObject: template,
|
||||
contextData,
|
||||
useShopSpecificTemplate,
|
||||
});
|
||||
})()
|
||||
);
|
||||
});
|
||||
await Promise.all(proms);
|
||||
@@ -172,8 +177,8 @@ export async function RenderTemplates(
|
||||
|
||||
unsortedTemplatesAndData.sort(function (a, b) {
|
||||
return (
|
||||
templateObjects.findIndex((x) => x.name === a.templateObject.name) -
|
||||
templateObjects.findIndex((x) => x.name === b.templateObject.name)
|
||||
templateObjects.findIndex((x) => x.name === a.templateObject.name) -
|
||||
templateObjects.findIndex((x) => x.name === b.templateObject.name)
|
||||
);
|
||||
});
|
||||
const templateAndData = unsortedTemplatesAndData;
|
||||
@@ -183,25 +188,25 @@ export async function RenderTemplates(
|
||||
let reportRequest = {
|
||||
template: {
|
||||
name: rootTemplate.useShopSpecificTemplate
|
||||
? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
|
||||
: `/${rootTemplate.templateObject.name}`,
|
||||
? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
|
||||
: `/${rootTemplate.templateObject.name}`,
|
||||
...(renderAsHtml
|
||||
? {}
|
||||
: {
|
||||
? {}
|
||||
: {
|
||||
recipe: "chrome-pdf",
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
},
|
||||
}),
|
||||
pdfOperations: [
|
||||
@@ -218,22 +223,22 @@ export async function RenderTemplates(
|
||||
template: {
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
},
|
||||
name: template.useShopSpecificTemplate
|
||||
? `/${bodyshop.imexshopid}/${template.templateObject.name}`
|
||||
: `/${template.templateObject.name}`,
|
||||
...(renderAsHtml ? {} : { recipe: "chrome-pdf" }),
|
||||
? `/${bodyshop.imexshopid}/${template.templateObject.name}`
|
||||
: `/${template.templateObject.name}`,
|
||||
...(renderAsHtml ? {} : {recipe: "chrome-pdf"}),
|
||||
},
|
||||
type: "append",
|
||||
|
||||
@@ -245,8 +250,8 @@ export async function RenderTemplates(
|
||||
},
|
||||
data: {
|
||||
...extend(
|
||||
rootTemplate.contextData,
|
||||
...templateAndData.map((temp) => temp.contextData)
|
||||
rootTemplate.contextData,
|
||||
...templateAndData.map((temp) => temp.contextData)
|
||||
),
|
||||
|
||||
// ...rootTemplate.templateObject.variables,
|
||||
@@ -266,32 +271,33 @@ export async function RenderTemplates(
|
||||
return render.toString();
|
||||
}
|
||||
} catch (error) {
|
||||
notification["error"]({ message: JSON.stringify(error) });
|
||||
notification["error"]({message: JSON.stringify(error)});
|
||||
}
|
||||
}
|
||||
|
||||
export const GenerateDocument = async (
|
||||
template,
|
||||
messageOptions,
|
||||
sendType,
|
||||
jobid
|
||||
template,
|
||||
messageOptions,
|
||||
sendType,
|
||||
jobid
|
||||
) => {
|
||||
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
|
||||
if (sendType === "e") {
|
||||
store.dispatch(
|
||||
setEmailOptions({
|
||||
jobid,
|
||||
messageOptions: {
|
||||
...messageOptions,
|
||||
to: Array.isArray(messageOptions.to)
|
||||
? messageOptions.to
|
||||
: [messageOptions.to],
|
||||
},
|
||||
template,
|
||||
})
|
||||
setEmailOptions({
|
||||
jobid,
|
||||
messageOptions: {
|
||||
...messageOptions,
|
||||
to: Array.isArray(messageOptions.to)
|
||||
? messageOptions.to
|
||||
: [messageOptions.to],
|
||||
},
|
||||
template,
|
||||
})
|
||||
);
|
||||
} else if (sendType === "x") {
|
||||
console.log("excel");
|
||||
await RenderTemplate(template, bodyshop, false, true);
|
||||
} else if (sendType === "text") {
|
||||
await RenderTemplate(template, bodyshop, false, false, true);
|
||||
@@ -305,22 +311,74 @@ export const GenerateDocuments = async (templates) => {
|
||||
await RenderTemplates(templates, bodyshop);
|
||||
};
|
||||
|
||||
export const fetchFilterData = async ({name}) => {
|
||||
try {
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
const jsrAuth = (await axios.post("/utils/jsr")).data;
|
||||
jsreport.headers["FirebaseAuthorization"] =
|
||||
"Bearer " + (await auth.currentUser.getIdToken());
|
||||
|
||||
const folders = await cleanAxios.get(`${server}/odata/folders`, {
|
||||
headers: {Authorization: jsrAuth},
|
||||
});
|
||||
const shopSpecificFolder = folders.data.value.find(
|
||||
(f) => f.name === bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const jsReportFilters = await cleanAxios.get(
|
||||
`${server}/odata/assets?$filter=name eq '${name}.filters'`,
|
||||
{headers: {Authorization: jsrAuth}}
|
||||
);
|
||||
console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
|
||||
|
||||
let parsedFilterData;
|
||||
let useShopSpecificTemplate = false;
|
||||
// let shopSpecificTemplate;
|
||||
|
||||
if (shopSpecificFolder) {
|
||||
let shopSpecificTemplate = jsReportFilters.data.value.find(
|
||||
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||
);
|
||||
if (shopSpecificTemplate) {
|
||||
useShopSpecificTemplate = true;
|
||||
parsedFilterData = atob(shopSpecificTemplate.content);
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedFilterData) {
|
||||
const generalTemplate = jsReportFilters.data.value.find((f) => !f.folder);
|
||||
useShopSpecificTemplate = false;
|
||||
if (generalTemplate) parsedFilterData = atob(generalTemplate.content);
|
||||
}
|
||||
const data = JSON.parse(parsedFilterData);
|
||||
return {
|
||||
data,
|
||||
useShopSpecificTemplate,
|
||||
success: true,
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchContextData = async (templateObject, jsrAuth) => {
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
|
||||
jsreport.headers["FirebaseAuthorization"] =
|
||||
"Bearer " + (await auth.currentUser.getIdToken());
|
||||
"Bearer " + (await auth.currentUser.getIdToken());
|
||||
|
||||
const folders = await cleanAxios.get(`${server}/odata/folders`, {
|
||||
headers: { Authorization: jsrAuth },
|
||||
headers: {Authorization: jsrAuth},
|
||||
});
|
||||
const shopSpecificFolder = folders.data.value.find(
|
||||
(f) => f.name === bodyshop.imexshopid
|
||||
(f) => f.name === bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const jsReportQueries = await cleanAxios.get(
|
||||
`${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
|
||||
{ headers: { Authorization: jsrAuth } }
|
||||
`${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
|
||||
{headers: {Authorization: jsrAuth}}
|
||||
);
|
||||
|
||||
let templateQueryToExecute;
|
||||
@@ -329,7 +387,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
|
||||
|
||||
if (shopSpecificFolder) {
|
||||
let shopSpecificTemplate = jsReportQueries.data.value.find(
|
||||
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||
);
|
||||
if (shopSpecificTemplate) {
|
||||
useShopSpecificTemplate = true;
|
||||
@@ -343,16 +401,35 @@ const fetchContextData = async (templateObject, jsrAuth) => {
|
||||
templateQueryToExecute = atob(generalTemplate.content);
|
||||
}
|
||||
|
||||
let contextData = {};
|
||||
if (templateQueryToExecute) {
|
||||
const { data } = await client.query({
|
||||
query: gql(templateQueryToExecute),
|
||||
variables: { ...templateObject.variables },
|
||||
});
|
||||
contextData = data;
|
||||
// Commented out for future revision debugging
|
||||
// console.log('Template Object');
|
||||
// console.dir(templateObject);
|
||||
// console.log('Unmodified Query');
|
||||
// console.dir(templateQueryToExecute);
|
||||
|
||||
const hasFilters = templateObject?.filters?.length > 0;
|
||||
const hasSorters = templateObject?.sorters?.length > 0;
|
||||
const hasDefaultSorters = templateObject?.defaultSorters?.length > 0;
|
||||
|
||||
// We have no template filters or sorters, so we can just execute the query and return the data
|
||||
if (!hasFilters && !hasSorters && !hasDefaultSorters) {
|
||||
let contextData = {};
|
||||
if (templateQueryToExecute) {
|
||||
const {data} = await client.query({
|
||||
query: gql(templateQueryToExecute),
|
||||
variables: {...templateObject.variables},
|
||||
});
|
||||
contextData = data;
|
||||
}
|
||||
|
||||
return {contextData, useShopSpecificTemplate};
|
||||
}
|
||||
|
||||
return { contextData, useShopSpecificTemplate };
|
||||
return await generateTemplate(
|
||||
templateQueryToExecute,
|
||||
templateObject,
|
||||
useShopSpecificTemplate
|
||||
);
|
||||
};
|
||||
|
||||
//export const displayTemplateInWindow = (html) => {
|
||||
@@ -389,7 +466,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
|
||||
|
||||
function extend(o1, o2, o3) {
|
||||
var result = {},
|
||||
obj;
|
||||
obj;
|
||||
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
obj = arguments[i];
|
||||
@@ -405,4 +482,4 @@ function extend(o1, o2, o3) {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
468
client/src/utils/graphQLmodifier.js
Normal file
468
client/src/utils/graphQLmodifier.js
Normal file
@@ -0,0 +1,468 @@
|
||||
import {Kind, parse, print, visit} from "graphql";
|
||||
import client from "./GraphQLClient";
|
||||
import {gql} from "@apollo/client";
|
||||
|
||||
/* eslint-disable no-loop-func */
|
||||
|
||||
/**
|
||||
* The available operators for filtering (string)
|
||||
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
|
||||
*/
|
||||
const STRING_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_like", label: "contains"},
|
||||
{value: "_nlike", label: "does not contain"},
|
||||
{value: "_ilike", label: "contains case-insensitive"},
|
||||
{value: "_nilike", label: "does not contain case-insensitive"},
|
||||
{value: "_in", label: "in", type: "array"},
|
||||
{value: "_nin", label: "not in", type: "array"}
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for filtering (dates)
|
||||
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
|
||||
*/
|
||||
const DATE_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_gt", label: "greater than"},
|
||||
{value: "_lt", label: "less than"},
|
||||
{value: "_gte", label: "greater than or equal"},
|
||||
{value: "_lte", label: "less than or equal"},
|
||||
{value: "_in", label: "in", type: "array"},
|
||||
{value: "_nin", label: "not in", type: "array"}
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for filtering (booleans)
|
||||
* @type {[{label: string, value: string},{label: string, value: string}]}
|
||||
*/
|
||||
const BOOLEAN_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for filtering (numbers)
|
||||
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
|
||||
*/
|
||||
const NUMBER_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_gt", label: "greater than"},
|
||||
{value: "_lt", label: "less than"},
|
||||
{value: "_gte", label: "greater than or equal"},
|
||||
{value: "_lte", label: "less than or equal"},
|
||||
{value: "_in", label: "in", type: "array"},
|
||||
{value: "_nin", label: "not in", type: "array"}
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for sorting
|
||||
* @type {[{label: string, value: string},{label: string, value: string}]}
|
||||
*/
|
||||
const ORDER_BY_OPERATORS = [
|
||||
{value: "asc", label: "ascending"},
|
||||
{value: "desc", label: "descending"}
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the available operators for filtering
|
||||
* @returns {[{label: string, value: string},{label: string, value: string}]}
|
||||
*/
|
||||
export function getOrderOperatorsByType() {
|
||||
return ORDER_BY_OPERATORS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available operators for filtering
|
||||
* @param type
|
||||
* @returns {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null]}
|
||||
*/
|
||||
export function getWhereOperatorsByType(type = 'string') {
|
||||
const operators = {
|
||||
string: STRING_OPERATORS,
|
||||
number: NUMBER_OPERATORS,
|
||||
boolean: BOOLEAN_OPERATORS,
|
||||
bool: BOOLEAN_OPERATORS,
|
||||
date: DATE_OPERATORS
|
||||
};
|
||||
return operators[type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a GraphQL query into an AST
|
||||
* @param query
|
||||
* @returns {DocumentNode}
|
||||
*/
|
||||
export function parseQuery(query) {
|
||||
return parse(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an AST back into a GraphQL query
|
||||
* @param query
|
||||
* @returns {string}
|
||||
*/
|
||||
export function printQuery(query) {
|
||||
return print(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a template based on the query and object
|
||||
* @param templateQueryToExecute
|
||||
* @param templateObject
|
||||
* @param useShopSpecificTemplate
|
||||
* @returns {Promise<{contextData: {}, useShopSpecificTemplate}>}
|
||||
*/
|
||||
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) {
|
||||
// Advanced Filtering and Sorting modifications start here
|
||||
|
||||
// Parse the query and apply the filters and sorters
|
||||
const ast = parseQuery(templateQueryToExecute);
|
||||
|
||||
|
||||
if (templateObject?.filters && templateObject?.filters?.length) {
|
||||
applyFilters(ast, templateObject.filters);
|
||||
}
|
||||
|
||||
if (templateObject?.sorters && templateObject?.sorters?.length) {
|
||||
applySorters(ast, templateObject.sorters);
|
||||
} else if (templateObject?.defaultSorters && templateObject?.defaultSorters?.length) {
|
||||
applySorters(ast, templateObject.defaultSorters);
|
||||
}
|
||||
|
||||
const finalQuery = printQuery(ast);
|
||||
|
||||
// commented out for future revision debugging
|
||||
// console.log('Modified Query');
|
||||
// console.log(finalQuery);
|
||||
|
||||
let contextData = {};
|
||||
if (templateQueryToExecute) {
|
||||
const {data} = await client.query({
|
||||
query: gql(finalQuery),
|
||||
variables: {...templateObject.variables},
|
||||
});
|
||||
contextData = data;
|
||||
}
|
||||
|
||||
return {contextData, useShopSpecificTemplate};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply sorters to the AST
|
||||
* @param ast
|
||||
* @param sorters
|
||||
*/
|
||||
export function applySorters(ast, sorters) {
|
||||
sorters.forEach((sorter) => {
|
||||
const fieldPath = sorter.field.split('.');
|
||||
visit(ast, {
|
||||
OperationDefinition: {
|
||||
enter(node) {
|
||||
// Loop through each sorter to apply it
|
||||
// noinspection DuplicatedCode
|
||||
|
||||
let currentSelection = node; // Start with the root operation
|
||||
|
||||
// Navigate down the field path to the correct location
|
||||
for (let i = 0; i < fieldPath.length - 1; i++) {
|
||||
let found = false;
|
||||
visit(currentSelection, {
|
||||
Field: {
|
||||
enter(node) {
|
||||
if (node.name.value === fieldPath[i]) {
|
||||
currentSelection = node; // Move down to the next level
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!found) break; // Stop if we can't find the next field in the path
|
||||
}
|
||||
|
||||
// Apply the sorter at the correct level
|
||||
if (currentSelection) {
|
||||
const targetFieldName = fieldPath[fieldPath.length - 1];
|
||||
let orderByArg = currentSelection.arguments.find(arg => arg.name.value === 'order_by');
|
||||
if (!orderByArg) {
|
||||
orderByArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'order_by'},
|
||||
value: {kind: Kind.OBJECT, fields: []},
|
||||
};
|
||||
currentSelection.arguments.push(orderByArg);
|
||||
}
|
||||
|
||||
const sorterField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: targetFieldName},
|
||||
value: {kind: Kind.ENUM, value: sorter.direction}, // Adjust if your schema uses a different type for sorting directions
|
||||
};
|
||||
|
||||
// Add the new sorter condition
|
||||
orderByArg.value.fields.push(sorterField);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Top Level Sub to the AST
|
||||
* @param node
|
||||
* @param fieldPath
|
||||
* @param filterField
|
||||
*/
|
||||
function applyTopLevelSub(node, fieldPath, filterField) {
|
||||
// Find or create the where argument for the top-level subfield
|
||||
let whereArg = node.selectionSet.selections
|
||||
.find(selection => selection.name.value === fieldPath[0])
|
||||
?.arguments.find(arg => arg.name.value === 'where');
|
||||
|
||||
if (!whereArg) {
|
||||
whereArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []},
|
||||
};
|
||||
const topLevelSubSelection = node.selectionSet.selections.find(selection =>
|
||||
selection.name.value === fieldPath[0]
|
||||
);
|
||||
if (topLevelSubSelection) {
|
||||
topLevelSubSelection.arguments = topLevelSubSelection.arguments || [];
|
||||
topLevelSubSelection.arguments.push(whereArg);
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly position the nested filter without an extra 'where'
|
||||
if (fieldPath.length > 2) { // More than one level deep
|
||||
let currentField = whereArg.value;
|
||||
fieldPath.slice(1, -1).forEach((path, index) => {
|
||||
let existingField = currentField.fields.find(f => f.name.value === path);
|
||||
if (!existingField) {
|
||||
existingField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: path},
|
||||
value: {kind: Kind.OBJECT, fields: []}
|
||||
};
|
||||
currentField.fields.push(existingField);
|
||||
}
|
||||
currentField = existingField.value;
|
||||
});
|
||||
currentField.fields.push(filterField);
|
||||
} else { // Directly under the top level
|
||||
whereArg.value.fields.push(filterField);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to the AST
|
||||
* @param ast
|
||||
* @param filters
|
||||
* @returns {ASTNode}
|
||||
*/
|
||||
export function applyFilters(ast, filters) {
|
||||
return visit(ast, {
|
||||
OperationDefinition: {
|
||||
enter(node) {
|
||||
filters.forEach(filter => {
|
||||
const fieldPath = filter.field.split('.');
|
||||
let topLevel = false;
|
||||
let topLevelSub = false;
|
||||
|
||||
// Determine if the filter should be applied at the top level
|
||||
if (fieldPath.length === 2) {
|
||||
topLevel = true;
|
||||
}
|
||||
|
||||
if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
|
||||
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
|
||||
topLevelSub = true;
|
||||
}
|
||||
|
||||
// Construct the filter for a top-level application
|
||||
const targetFieldName = fieldPath[fieldPath.length - 1];
|
||||
|
||||
let filterValue = createFilterValue(filter);
|
||||
let filterField = createFilterField(targetFieldName, filter, filterValue);
|
||||
|
||||
if (topLevel) {
|
||||
applyTopLevelFilter(node, fieldPath, filterField);
|
||||
} else if (topLevelSub) {
|
||||
applyTopLevelSub(node, fieldPath, filterField);
|
||||
} else {
|
||||
applyNestedFilter(node, fieldPath, filterField);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter value based on the filter
|
||||
* @param filter
|
||||
* @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
|
||||
*/
|
||||
function createFilterValue(filter) {
|
||||
if (Array.isArray(filter.value)) {
|
||||
// If it's an array, create a list value with the array items
|
||||
return {
|
||||
kind: Kind.LIST,
|
||||
values: filter.value.map(item => ({
|
||||
kind: getGraphQLKind(item),
|
||||
value: item,
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
// If it's not an array, use the existing logic
|
||||
return {
|
||||
kind: getGraphQLKind(filter.value),
|
||||
value: filter.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter field based on the target field and filter
|
||||
* @param targetFieldName
|
||||
* @param filter
|
||||
* @param filterValue
|
||||
* @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}}
|
||||
*/
|
||||
function createFilterField(targetFieldName, filter, filterValue) {
|
||||
return {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: targetFieldName},
|
||||
value: {
|
||||
kind: Kind.OBJECT,
|
||||
fields: [{
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: filter.operator},
|
||||
value: filterValue,
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a top-level filter to the AST
|
||||
* @param node
|
||||
* @param fieldPath
|
||||
* @param filterField
|
||||
*/
|
||||
function applyTopLevelFilter(node, fieldPath, filterField) {
|
||||
// Find or create the where argument for the top-level field
|
||||
let whereArg = node.selectionSet.selections
|
||||
.find(selection => selection.name.value === fieldPath[0])
|
||||
?.arguments.find(arg => arg.name.value === 'where');
|
||||
|
||||
if (!whereArg) {
|
||||
whereArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []},
|
||||
};
|
||||
const topLevelSelection = node.selectionSet.selections.find(selection =>
|
||||
selection.name.value === fieldPath[0]
|
||||
);
|
||||
if (topLevelSelection) {
|
||||
topLevelSelection.arguments = topLevelSelection.arguments || [];
|
||||
topLevelSelection.arguments.push(whereArg);
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly position the nested filter without an extra 'where'
|
||||
if (fieldPath.length > 2) { // More than one level deep
|
||||
let currentField = whereArg.value;
|
||||
fieldPath.slice(1, -1).forEach((path, index) => {
|
||||
let existingField = currentField.fields.find(f => f.name.value === path);
|
||||
if (!existingField) {
|
||||
existingField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: path},
|
||||
value: {kind: Kind.OBJECT, fields: []}
|
||||
};
|
||||
currentField.fields.push(existingField);
|
||||
}
|
||||
currentField = existingField.value;
|
||||
});
|
||||
currentField.fields.push(filterField);
|
||||
} else { // Directly under the top level
|
||||
whereArg.value.fields.push(filterField);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a nested filter to the AST
|
||||
* @param node
|
||||
* @param fieldPath
|
||||
* @param filterField
|
||||
*/
|
||||
function applyNestedFilter(node, fieldPath, filterField) {
|
||||
// Initialize a reference to the current selection to traverse down the AST
|
||||
let currentSelection = node;
|
||||
|
||||
// Iterate over the fieldPath, except for the last entry, to navigate the structure
|
||||
for (let i = 0; i < fieldPath.length - 1; i++) {
|
||||
const fieldName = fieldPath[i];
|
||||
let fieldFound = false;
|
||||
|
||||
// Check if the current selection has a selectionSet and selections
|
||||
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
|
||||
// Look for the field in the current selection's selections
|
||||
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
|
||||
if (selection) {
|
||||
// Move down the AST to the found selection
|
||||
currentSelection = selection;
|
||||
fieldFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the field was not found in the current path, it's an issue
|
||||
if (!fieldFound) {
|
||||
console.error(`Field ${fieldName} not found in the current selection.`);
|
||||
return; // Exit the loop and function due to error
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, currentSelection should be the parent field where the filter needs to be applied
|
||||
// Check if the 'where' argument already exists in the current selection
|
||||
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
|
||||
if (!whereArg) {
|
||||
// If not found, create a new 'where' argument for the current selection
|
||||
currentSelection.arguments.push({
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
|
||||
});
|
||||
}
|
||||
|
||||
// Add the filter field to the 'where' clause of the current selection
|
||||
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GraphQL kind for a value
|
||||
* @param value
|
||||
* @returns {Kind|Kind.INT}
|
||||
*/
|
||||
function getGraphQLKind(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return Kind.LIST;
|
||||
} else if (typeof value === 'number') {
|
||||
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return Kind.BOOLEAN;
|
||||
} else if (typeof value === 'string') {
|
||||
return Kind.STRING;
|
||||
} else if (value instanceof Date) {
|
||||
return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable no-loop-func */
|
||||
@@ -259,28 +259,30 @@
|
||||
- active:
|
||||
_eq: true
|
||||
columns:
|
||||
- id
|
||||
- billid
|
||||
- bodyshopid
|
||||
- created
|
||||
- operation
|
||||
- id
|
||||
- jobid
|
||||
- new_val
|
||||
- old_val
|
||||
- operation
|
||||
- type
|
||||
- useremail
|
||||
- bodyshopid
|
||||
- jobid
|
||||
- billid
|
||||
select_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- billid
|
||||
- bodyshopid
|
||||
- created
|
||||
- id
|
||||
- jobid
|
||||
- new_val
|
||||
- old_val
|
||||
- operation
|
||||
- type
|
||||
- useremail
|
||||
- created
|
||||
- billid
|
||||
- bodyshopid
|
||||
- jobid
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
@@ -567,6 +569,13 @@
|
||||
table:
|
||||
name: parts_orders
|
||||
schema: public
|
||||
- name: tasks
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: billid
|
||||
table:
|
||||
name: tasks
|
||||
schema: public
|
||||
insert_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
@@ -816,6 +825,13 @@
|
||||
table:
|
||||
name: inventory
|
||||
schema: public
|
||||
- name: ioevents
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: bodyshopid
|
||||
table:
|
||||
name: ioevents
|
||||
schema: public
|
||||
- name: jobs
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
@@ -844,6 +860,13 @@
|
||||
table:
|
||||
name: phonebook
|
||||
schema: public
|
||||
- name: tasks
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: bodyshopid
|
||||
table:
|
||||
name: tasks
|
||||
schema: public
|
||||
- name: timetickets
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
@@ -2673,6 +2696,13 @@
|
||||
- table:
|
||||
name: ioevents
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: bodyshop
|
||||
using:
|
||||
foreign_key_constraint_on: bodyshopid
|
||||
- name: user
|
||||
using:
|
||||
foreign_key_constraint_on: useremail
|
||||
- table:
|
||||
name: job_ar_schema
|
||||
schema: public
|
||||
@@ -2822,6 +2852,13 @@
|
||||
table:
|
||||
name: parts_order_lines
|
||||
schema: public
|
||||
- name: tasks
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: joblineid
|
||||
table:
|
||||
name: tasks
|
||||
schema: public
|
||||
insert_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
@@ -3309,6 +3346,13 @@
|
||||
table:
|
||||
name: scoreboard
|
||||
schema: public
|
||||
- name: tasks
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: jobid
|
||||
table:
|
||||
name: tasks
|
||||
schema: public
|
||||
- name: timetickets
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
@@ -5006,6 +5050,13 @@
|
||||
table:
|
||||
name: parts_order_lines
|
||||
schema: public
|
||||
- name: tasks
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: partsorderid
|
||||
table:
|
||||
name: tasks
|
||||
schema: public
|
||||
insert_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
@@ -5621,6 +5672,128 @@
|
||||
_eq: X-Hasura-User-Id
|
||||
- active:
|
||||
_eq: true
|
||||
- table:
|
||||
name: tasks
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: bill
|
||||
using:
|
||||
foreign_key_constraint_on: billid
|
||||
- name: bodyshop
|
||||
using:
|
||||
foreign_key_constraint_on: bodyshopid
|
||||
- name: job
|
||||
using:
|
||||
foreign_key_constraint_on: jobid
|
||||
- name: jobline
|
||||
using:
|
||||
foreign_key_constraint_on: joblineid
|
||||
- name: parts_order
|
||||
using:
|
||||
foreign_key_constraint_on: partsorderid
|
||||
- name: user
|
||||
using:
|
||||
foreign_key_constraint_on: assigned_to
|
||||
- name: userByCreatedBy
|
||||
using:
|
||||
foreign_key_constraint_on: created_by
|
||||
insert_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
check:
|
||||
bodyshop:
|
||||
associations:
|
||||
_and:
|
||||
- user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
- active:
|
||||
_eq: true
|
||||
columns:
|
||||
- completed
|
||||
- deleted
|
||||
- priority
|
||||
- assigned_to
|
||||
- created_by
|
||||
- description
|
||||
- title
|
||||
- completed_at
|
||||
- created_at
|
||||
- deleted_at
|
||||
- due_date
|
||||
- remind_at
|
||||
- updated_at
|
||||
- billid
|
||||
- bodyshopid
|
||||
- id
|
||||
- jobid
|
||||
- joblineid
|
||||
- partsorderid
|
||||
select_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- completed
|
||||
- deleted
|
||||
- priority
|
||||
- assigned_to
|
||||
- created_by
|
||||
- description
|
||||
- title
|
||||
- completed_at
|
||||
- created_at
|
||||
- deleted_at
|
||||
- due_date
|
||||
- remind_at
|
||||
- updated_at
|
||||
- billid
|
||||
- bodyshopid
|
||||
- id
|
||||
- jobid
|
||||
- joblineid
|
||||
- partsorderid
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
_and:
|
||||
- user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
- active:
|
||||
_eq: true
|
||||
update_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- completed
|
||||
- deleted
|
||||
- priority
|
||||
- assigned_to
|
||||
- created_by
|
||||
- description
|
||||
- title
|
||||
- completed_at
|
||||
- created_at
|
||||
- deleted_at
|
||||
- due_date
|
||||
- remind_at
|
||||
- updated_at
|
||||
- billid
|
||||
- bodyshopid
|
||||
- id
|
||||
- jobid
|
||||
- joblineid
|
||||
- partsorderid
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
_and:
|
||||
- user:
|
||||
authid:
|
||||
_eq: X-Hasura-User-Id
|
||||
- active:
|
||||
_eq: true
|
||||
check: null
|
||||
- table:
|
||||
name: timetickets
|
||||
schema: public
|
||||
@@ -6004,6 +6177,13 @@
|
||||
table:
|
||||
name: exportlog
|
||||
schema: public
|
||||
- name: ioevents
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: useremail
|
||||
table:
|
||||
name: ioevents
|
||||
schema: public
|
||||
- name: messages
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
@@ -6032,6 +6212,20 @@
|
||||
table:
|
||||
name: parts_orders
|
||||
schema: public
|
||||
- name: tasks
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: assigned_to
|
||||
table:
|
||||
name: tasks
|
||||
schema: public
|
||||
- name: tasksByCreatedBy
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: created_by
|
||||
table:
|
||||
name: tasks
|
||||
schema: public
|
||||
- name: timetickets
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."audit_trail" add column "type" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."audit_trail" add column "type" text
|
||||
null;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE "public"."tasks";
|
||||
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE "public"."tasks" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "title" text NOT NULL, "description" Text, "deleted" boolean NOT NULL DEFAULT false, "deleted_at" timestamptz, "due_date" timestamptz, "created_by" text NOT NULL, "assigned_to" Text, "completed" boolean NOT NULL DEFAULT false, "completed_at" timestamptz, "remind_at" timestamptz, "priority" numeric, "bodyshopid" UUID NOT NULL, "jobid" UUID NOT NULL, "joblineid" UUID, "partsorderid" UUID, "billid" UUID, PRIMARY KEY ("id") , FOREIGN KEY ("created_by") REFERENCES "public"."users"("email") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("assigned_to") REFERENCES "public"."users"("email") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("joblineid") REFERENCES "public"."joblines"("id") ON UPDATE set null ON DELETE set null, FOREIGN KEY ("partsorderid") REFERENCES "public"."parts_orders"("id") ON UPDATE set null ON DELETE set null, FOREIGN KEY ("billid") REFERENCES "public"."bills"("id") ON UPDATE set null ON DELETE set null);
|
||||
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
_new record;
|
||||
BEGIN
|
||||
_new := NEW;
|
||||
_new."updated_at" = NOW();
|
||||
RETURN _new;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER "set_public_tasks_updated_at"
|
||||
BEFORE UPDATE ON "public"."tasks"
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
|
||||
COMMENT ON TRIGGER "set_public_tasks_updated_at" ON "public"."tasks"
|
||||
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
748
package-lock.json
generated
748
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@aws-sdk/client-secrets-manager": "^3.454.0",
|
||||
"@aws-sdk/client-ses": "^3.454.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.451.0",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@opensearch-project/opensearch": "^2.4.0",
|
||||
"aws4": "^1.12.0",
|
||||
"axios": "^1.6.2",
|
||||
@@ -52,6 +53,7 @@
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
},
|
||||
@@ -695,11 +697,496 @@
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"node_modules/@azure/abort-controller": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz",
|
||||
"integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-auth": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.6.0.tgz",
|
||||
"integrity": "sha512-3X9wzaaGgRaBCwhLQZDtFp5uLIXCPrGbwJNWPPugvL4xbIGgScv77YzzxToKGLAKvG9amDoofMoP+9hsH1vs1w==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-util": "^1.1.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-auth/node_modules/@azure/abort-controller": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
|
||||
"integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz",
|
||||
"integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-auth": "^1.3.0",
|
||||
"@azure/core-tracing": "1.0.0-preview.13",
|
||||
"@azure/core-util": "^1.1.1",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"@types/node-fetch": "^2.5.0",
|
||||
"@types/tunnel": "^0.0.3",
|
||||
"form-data": "^4.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"process": "^0.11.10",
|
||||
"tslib": "^2.2.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"uuid": "^8.3.0",
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/xml2js": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-lro": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.6.0.tgz",
|
||||
"integrity": "sha512-PyRNcaIOfMgoUC01/24NoG+k8O81VrKxYARnDlo+Q2xji0/0/j2nIt8BwQh294pb1c5QnXTDPbNR4KzoDKXEoQ==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-util": "^1.2.0",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-lro/node_modules/@azure/abort-controller": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
|
||||
"integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-paging": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz",
|
||||
"integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-tracing": {
|
||||
"version": "1.0.0-preview.13",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz",
|
||||
"integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.0.1",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-util": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.7.0.tgz",
|
||||
"integrity": "sha512-Zq2i3QO6k9DA8vnm29mYM4G8IE9u1mhF1GUabVEqPNX8Lj833gdxQ2NAFxt2BZsfAL+e9cT8SyVN7dFVJ/Hf0g==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-util/node_modules/@azure/abort-controller": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
|
||||
"integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/logger": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz",
|
||||
"integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/storage-blob": {
|
||||
"version": "12.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz",
|
||||
"integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-http": "^3.0.0",
|
||||
"@azure/core-lro": "^2.2.0",
|
||||
"@azure/core-paging": "^1.1.1",
|
||||
"@azure/core-tracing": "1.0.0-preview.13",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"events": "^3.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
|
||||
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.23.4",
|
||||
"chalk": "^2.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.17.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
|
||||
"integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.17.0",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator/node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-environment-visitor": {
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
|
||||
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-function-name": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
|
||||
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/types": "^7.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-function-name/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
|
||||
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-split-export-declaration": {
|
||||
"version": "7.22.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
|
||||
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz",
|
||||
"integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==",
|
||||
"optional": true,
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
|
||||
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
|
||||
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"chalk": "^2.4.2",
|
||||
"js-tokens": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
|
||||
"integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
@@ -718,6 +1205,97 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
|
||||
"integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
"@babel/parser": "^7.23.9",
|
||||
"@babel/types": "^7.23.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
|
||||
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
"@babel/helper-function-name": "^7.23.0",
|
||||
"@babel/helper-hoist-variables": "^7.22.5",
|
||||
"@babel/helper-split-export-declaration": "^7.22.6",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/types": "^7.23.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse/node_modules/@babel/generator": {
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
|
||||
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.23.6",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"jsesc": "^2.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
||||
"integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
@@ -1034,6 +1612,54 @@
|
||||
"resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz",
|
||||
"integrity": "sha512-bIcKnH5YmtTYr7S6J3J86dn/rFiklwRpOqbTOQ9C0WMmR9FKHVb3bxs2UYfqEmNb93O4nbA97sb6rtz33i9SyA=="
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz",
|
||||
"integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.23",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz",
|
||||
"integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdoc/salty": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz",
|
||||
@@ -1106,6 +1732,14 @@
|
||||
"yarn": "^1.22.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz",
|
||||
"integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -1714,6 +2348,29 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz",
|
||||
"integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/generator": "7.17.7",
|
||||
"@babel/parser": "^7.20.5",
|
||||
"@babel/traverse": "7.23.2",
|
||||
"@babel/types": "7.17.0",
|
||||
"javascript-natural-sort": "0.7.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/compiler-sfc": "3.x",
|
||||
"prettier": "2.x - 3.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/compiler-sfc": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
@@ -1857,6 +2514,15 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
|
||||
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
|
||||
@@ -1906,6 +2572,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
|
||||
},
|
||||
"node_modules/@types/tunnel": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz",
|
||||
"integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
@@ -3307,6 +3981,14 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
@@ -3854,6 +4536,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz",
|
||||
@@ -4446,6 +5137,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.15.4",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz",
|
||||
@@ -4459,6 +5156,12 @@
|
||||
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
|
||||
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
@@ -4540,6 +5243,18 @@
|
||||
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/json-2-csv": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.0.1.tgz",
|
||||
@@ -5457,6 +6172,14 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -6894,6 +7617,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -6949,6 +7681,14 @@
|
||||
"node": ">=0.6.x"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@aws-sdk/client-secrets-manager": "^3.454.0",
|
||||
"@aws-sdk/client-ses": "^3.454.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.451.0",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@opensearch-project/opensearch": "^2.4.0",
|
||||
"aws4": "^1.12.0",
|
||||
"axios": "^1.6.2",
|
||||
@@ -61,6 +62,7 @@
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ app.use('/adm', require("./server/routes/adminRoutes"));
|
||||
app.use('/tech', require("./server/routes/techRoutes"));
|
||||
app.use('/intellipay', require("./server/routes/intellipayRoutes"));
|
||||
app.use('/cdk', require("./server/routes/cdkRoutes"));
|
||||
app.use('/csi', require("./server/routes/csiRoutes"));
|
||||
|
||||
// Default route for forbidden access
|
||||
app.get("/", (req, res) => {
|
||||
|
||||
2
server/csi/csi.js
Normal file
2
server/csi/csi.js
Normal file
@@ -0,0 +1,2 @@
|
||||
exports.lookup = require("./lookup").default;
|
||||
exports.submit = require("./submit").default;
|
||||
24
server/csi/lookup.js
Normal file
24
server/csi/lookup.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const path = require("path");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(
|
||||
process.cwd(),
|
||||
`.env.${process.env.NODE_ENV || "development"}`
|
||||
),
|
||||
});
|
||||
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
try {
|
||||
logger.log("csi-surveyID-lookup", "DEBUG", "csi", req.body.surveyId, null);
|
||||
const gql_response = await client.request(queries.QUERY_SURVEY, {
|
||||
surveyId: req.body.surveyId,
|
||||
});
|
||||
res.status(200).json(gql_response);
|
||||
} catch (error) {
|
||||
logger.log("csi-surveyID-lookup", "ERROR", "csi", req.body.surveyId, error);
|
||||
res.status(400).json(error);
|
||||
}
|
||||
};
|
||||
29
server/csi/submit.js
Normal file
29
server/csi/submit.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const path = require("path");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(
|
||||
process.cwd(),
|
||||
`.env.${process.env.NODE_ENV || "development"}`
|
||||
),
|
||||
});
|
||||
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
try {
|
||||
logger.log("csi-surveyID-submit", "DEBUG", "csi", req.body.surveyId, null);
|
||||
const gql_response = await client.request(queries.COMPLETE_SURVEY, {
|
||||
surveyId: req.body.surveyId,
|
||||
survey: {
|
||||
response: req.body.values,
|
||||
valid: false,
|
||||
completedon: new Date(),
|
||||
},
|
||||
});
|
||||
res.status(200).json(gql_response);
|
||||
} catch (error) {
|
||||
logger.log("csi-surveyID-submit", "ERROR", "csi", req.body.surveyId, error);
|
||||
res.status(400).json(error);
|
||||
}
|
||||
};
|
||||
@@ -2156,3 +2156,23 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
|
||||
shopid
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.QUERY_SURVEY = `query QUERY_SURVEY($surveyId: uuid!) {
|
||||
csi_by_pk(id: $surveyId) {
|
||||
completedon
|
||||
csiquestion {
|
||||
id
|
||||
config
|
||||
}
|
||||
id
|
||||
relateddata
|
||||
valid
|
||||
validuntil
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.COMPLETE_SURVEY = `mutation COMPLETE_SURVEY($surveyId: uuid!, $survey: csi_set_input) {
|
||||
update_csi(where: { id: { _eq: $surveyId } }, _set: $survey) {
|
||||
affected_rows
|
||||
}
|
||||
}`;
|
||||
8
server/routes/csiRoutes.js
Normal file
8
server/routes/csiRoutes.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { lookup, submit } = require("../csi/csi");
|
||||
|
||||
router.post("/lookup", lookup);
|
||||
router.post("/submit", submit);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user