Compare commits

...

73 Commits

Author SHA1 Message Date
Allan Carr
b1ca09bd4f IO-2710 Prettierr
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:55:59 -07:00
Allan Carr
f9ca36ec89 IO-2710 Job Assignment
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-19 17:52:47 -07:00
Dave Richer
f98c9e6f71 Merged in release/2024-03-15 (pull request #1354)
Release - 2024 03 15

Approved-by: Allan Carr
2024-03-15 18:50:55 +00:00
Patrick Fic
28f2e8ad30 Merged in feature/IO-2679-interactivity-tracking (pull request #1358)
Feature/IO-2679 interactivity tracking
2024-03-15 17:25:59 +00:00
Patrick Fic
c27e206687 Add index to audit trail. 2024-03-15 10:24:00 -07:00
Patrick Fic
01fd253f1d Manual modification to hasura migration. 2024-03-15 10:23:13 -07:00
Patrick Fic
e67bc0d953 Merged in feature/IO-2679-interactivity-tracking (pull request #1356)
Add ioevent logging for events.
2024-03-15 16:56:37 +00:00
Patrick Fic
3eab3e2fb6 Add ioevent logging for events. 2024-03-15 09:55:14 -07:00
Dave Richer
3adf6b649b Merged in feature/IO-2678-Linkable-schedule (pull request #1351)
- Implement
2024-03-15 14:49:06 +00:00
Dave Richer
f8243aa2b3 - Implement
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-15 10:47:46 -04:00
Dave Richer
3c3f50d138 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1348)
- Fix bug
2024-03-14 18:59:37 +00:00
Dave Richer
4f7e1b81ac - Fix bug
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 14:59:06 -04:00
Dave Richer
806bdc4c70 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1346)
- Missing translation
2024-03-14 16:38:12 +00:00
Dave Richer
a0572a0cec - Missing translation
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:37:38 -04:00
Dave Richer
04cdf13e86 Merged in feature/IO-2650-Lifecycle-V2 (pull request #1344)
Feature/IO-2650 Lifecycle V2
2024-03-14 16:33:14 +00:00
Dave Richer
50349e91dc - remove duplicated code
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:32:40 -04:00
Dave Richer
9998a8f154 - Fix bug on humanReadable field we have not consumed until now.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-14 12:28:07 -04:00
Allan Carr
9dcbcb2a43 Merged in feature/IO-2671-Vehicle-Detail-Table (pull request #1343)
IO-2671 Add in appropriate sorters at same time

Approved-by: Dave Richer
2024-03-14 15:58:46 +00:00
Allan Carr
5773f7a0f3 Merged in feature/IO-2650-Lifecycle-Report (pull request #1345)
IO-2650 Job Lifecycle Report Center Reports

Approved-by: Dave Richer
2024-03-14 15:58:17 +00:00
Allan Carr
8614d88e71 Merged in feature/IO-2630-Parts-Queue-Mods (pull request #1342)
IO-2630 Adjust for onRow selection

Approved-by: Dave Richer
2024-03-14 15:57:45 +00:00
Allan Carr
fa5e26c52a IO-2650 Job Lifecycle Report Center Reports
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 17:22:42 -07:00
Dave Richer
90e1cbd390 - Job Lifecycle Dashboard Component.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-13 19:20:10 -04:00
Allan Carr
947a3c6a88 IO-2671 Add in appropriate sorters at same time
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 12:31:51 -07:00
Allan Carr
a33662e6f0 IO-2630 Adjust for oRow selection
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 12:04:45 -07:00
Patrick Fic
6d1f04369e Merged in feature/IO-2674-reorder-resp-centers (pull request #1340)
IO-2674 Add reordering arrows for responsibility centers in IO.

Approved-by: Allan Carr
2024-03-13 17:07:09 +00:00
Patrick Fic
4a27726ef3 Adjusted label for payers & added reorder to payers. 2024-03-13 12:54:31 -04:00
Allan Carr
eedba97237 Merged in feature/IO-2625-BPT-Hrs-in-Employee-Assignment (pull request #1339)
IO-2625 B/P/T Hrs Display in Employee Assignment Block

Approved-by: Dave Richer
2024-03-13 16:50:47 +00:00
Allan Carr
bcf095ed4f Merged in feature/IO-2669-Next-Contact-Date-formating (pull request #1337)
IO-2669 Next Contact Formating

Approved-by: Dave Richer
2024-03-13 16:50:00 +00:00
Allan Carr
c3f9e268c7 Merged in feature/IO-2671-Vehicle-Detail-Table (pull request #1338)
IO-2671 Missing field in Vehicle query

Approved-by: Dave Richer
2024-03-13 16:49:36 +00:00
Allan Carr
c8442f0750 Merged in feature/IO-2570-Totals-Card-on-Draw-have-Customer-Owing (pull request #1341)
IO-2570 Change Ded to Customer Owing amount

Approved-by: Dave Richer
2024-03-13 16:49:13 +00:00
Allan Carr
da5b446c30 Merge branch 'master' into feature/IO-2630-Parts-Queue-Mods
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 09:49:02 -07:00
Allan Carr
e13b2bb969 Merged in feature/IO-2520-Kaizen-Data-Pump (pull request #1336)
IO-2520 Change where email notification occurs

Approved-by: Dave Richer
2024-03-13 16:48:01 +00:00
Allan Carr
e8969c4698 IO-2570 Change Ded to Customer Owing amount
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-13 09:47:33 -07:00
Patrick Fic
346d32a2bb IO-2674 Add reordering arrows for responsibility centers in IO. 2024-03-13 08:02:20 -04:00
Dave Richer
6ba00a90be Merge branch 'master' into feature/IO-2650-Lifecycle-V2 2024-03-12 18:11:08 -04:00
Dave Richer
4293d20313 - Backend Changes for Lifecycle Data
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-12 18:10:09 -04:00
Allan Carr
706c70c509 IO-2625 B/P/T Hrs Display in Employee Assignment Block
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 14:49:12 -07:00
Allan Carr
e872b1bf0a IO-2671 Missing field in Vehicle query
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 11:55:07 -07:00
Allan Carr
379fa060d8 IO-2669 Next Contact Formating
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-12 11:24:57 -07:00
Allan Carr
a6258c6456 Merged in feature/IO-2650-Lifecycle-Report (pull request #1335)
IO-2650 Lifecycle Report for Print Center

Approved-by: Dave Richer
2024-03-12 17:01:19 +00:00
Allan Carr
d6bf0a225b IO-2520 Change where email notification occurs
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:46:19 -07:00
Allan Carr
ec2b914e5e Merge branch 'master' into feature/IO-2520-Kaizen-Data-Pump
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:45:26 -07:00
Allan Carr
309a20148a IO-2650 Lifecycle Report for Print Center
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-11 17:41:37 -07:00
Dave Richer
013b56778b Merged in release/2024-03-08 (pull request #1331)
Release into Master for 03 08 Release

Approved-by: Allan Carr
2024-03-11 17:47:51 +00:00
Allan Carr
9dec4a3a61 Merged in feature/IO-2663-Export-Pages-Sorters-and-Filters (pull request #1327)
Correct Sorters
2024-03-08 19:36:40 +00:00
Allan Carr
189b4db90f Merged in feature/IO-2575-Production-Special-Coverage (pull request #1328)
Bring in line with other components
2024-03-08 19:36:27 +00:00
Allan Carr
c49fa1c527 Bring in line with other components
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-08 11:32:48 -08:00
Allan Carr
53ef048f6f Correct Sorters
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-08 11:24:41 -08:00
Dave Richer
1cc7eed983 Merged in feature/IO-2662-New-Report-Reflectors (pull request #1325)
- Add new special filters
2024-03-08 17:47:43 +00:00
Dave Richer
1c8f377212 - Add new special filters
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-08 12:47:11 -05:00
Dave Richer
c050947276 Merged in feature/IO-2662-New-Report-Reflectors (pull request #1323)
- Add new special filters
2024-03-08 17:37:10 +00:00
Dave Richer
9bde1f820d - Add new special filters
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-03-08 12:36:29 -05:00
Allan Carr
393765640c Merged in feature/IO-2663-Export-Pages-Sorters-and-Filters (pull request #1321)
IO-2663 Export Pages Sorters and Filters

Approved-by: Dave Richer
2024-03-08 01:14:40 +00:00
Allan Carr
69c2836425 Merged in feature/IO-2665-Jobs-sorters-and-query (pull request #1322)
IO-2665 Jobs page sorters and query adjustment

Approved-by: Dave Richer
2024-03-08 01:14:07 +00:00
Allan Carr
ec9edf30eb Merged in feature/IO-2651-Audit-Log-Extension (pull request #1320)
IO-2651 Audit Log Extension

Approved-by: Dave Richer
2024-03-08 01:13:42 +00:00
Allan Carr
6a812f9ea7 IO-2665 Jobs page sorters and query adjustment
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-07 11:53:34 -08:00
Allan Carr
fa86254bfd Prettierr
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-06 15:30:56 -08:00
Allan Carr
58ab7afbb3 IO-2663 Export Pages Sorters and Filters
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-06 15:29:24 -08:00
Allan Carr
fa7d90d2a9 IO-2651 Audit Log Extension
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-05 15:28:22 -08:00
Allan Carr
fbf9047974 Merged in feature/IO-2660-Phonebook-Draw-Title-Bug (pull request #1317)
IO-2660 Phonebook Drawer Title

Approved-by: Dave Richer
2024-03-05 20:32:29 +00:00
Allan Carr
3e9b795052 Merged in feature/IO-2575-Production-Special-Coverage (pull request #1318)
IO-2575 Special Coverage and Sorters & Filters

Approved-by: Dave Richer
2024-03-05 20:31:53 +00:00
Allan Carr
d29ffc21e5 IO-2575 Special Coverage and Sorters & Filters
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-05 11:49:57 -08:00
Allan Carr
959f7780e8 IO-2660 Phonebook Drawer Title
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-04 12:13:41 -08:00
Dave Richer
e47731702a Merged in release/2024-03-01 (pull request #1315)
Release/2024 03 01
2024-03-01 23:12:11 +00:00
Patrick Fic
85a3aeb335 Resolve refund payment logging. 2024-03-01 11:51:01 -08:00
Dave Richer
80b7ae0e54 Merge branch 'feautre/IO-2647-Reporting-V3-From-Master' into release/2024-03-01
# Conflicts:
#	_reference/reportFiltersAndSorters.md
2024-02-29 22:25:52 -05:00
Dave Richer
0529ac4478 - Reports V3 Targeted at Master
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-29 22:22:55 -05:00
Allan Carr
3cafbebbee Merged in feature/IO-1366-Audit-Logging (pull request #1311)
Feature/IO-1366 Audit Logging

Approved-by: Dave Richer
2024-03-01 02:44:05 +00:00
Allan Carr
6f248d864e Merged in feature/IO-2656-Job-Count-on-Scoreboard (pull request #1312)
IO-2656 Job Count on Scoreboard Jobs

Approved-by: Dave Richer
2024-03-01 02:43:23 +00:00
Allan Carr
a4a84572b7 IO-2656 Job Count on Scoreboard Jobs
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-29 15:36:58 -08:00
Allan Carr
0bc00d46cf Merged in feature/IO-2654-Courtesy-Car-List-Filter (pull request #1310)
IO-2654 Local Storage Filter State for Courtesy Car List

Approved-by: Dave Richer
2024-02-28 18:23:49 +00:00
Allan Carr
e80e40bb76 IO-2654 Local Storage Filter State for Courtesy Car List
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-27 16:37:07 -08:00
Allan Carr
a88c102b27 Prettier and Package Update
Azura Storage Blob and Trivago Prettier Sort Imports

Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-02-27 16:04:41 -08:00
97 changed files with 4473 additions and 869 deletions

16
.prettierrc.js Normal file
View 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,
};

View File

@@ -3,7 +3,13 @@
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically 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. modify the graphQL query and provide the user more power over their reports.
# Special Notes 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 - 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 ## High level Schema Overview
@@ -40,9 +46,10 @@ Filters effect the where clause of the graphQL query. They are used to filter th
A note on special notation used in the `name` field. A note on special notation used in the `name` field.
## Reflection ## Reflection
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file. 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", "name": "jobs.status",
"translation": "jobs.fields.status", "translation": "jobs.fields.status",
@@ -52,7 +59,7 @@ Filters can make use of reflection to pre-fill select boxes, the following is an
"type": "internal", "type": "internal",
"name": "special.job_statuses" "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` 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`
@@ -67,7 +74,13 @@ The following cases are available
- `special.employees` - This will reflect the employees `bodyshop.employees` - `special.employees` - This will reflect the employees `bodyshop.employees`
- `special.first_names` - This will reflect the first names `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.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 ### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",` `"name": "jobs.joblines.mod_lb_hrs",`
@@ -104,6 +117,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
``` ```
### Path with brackets,top level ### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",` `"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query. This will produce a where clause at the `jobs` level of the graphQL query.
@@ -138,18 +152,26 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
``` ```
## Known Caveats ## Known Caveats
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The `dates` object is not yet implemented and will be added in a future release. - Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
- The type object must be 'string' or 'number' and is case-sensitive. 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. - 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 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. - 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
- 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. - 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 ### 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. - 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`. - 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`.

File diff suppressed because it is too large Load Diff

View File

@@ -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 React, { useState } from "react";
import { useTranslation } from "react-i18next"; 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 { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors"; 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 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 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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -138,7 +138,6 @@ export function AccountingPayablesTableComponent({
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
key: "attempts", key: "attempts",
render: (text, record) => ( render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} /> <ExportLogsCountDisplay logs={record.exportlogs} />
), ),
@@ -147,8 +146,6 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<PayableExportButton <PayableExportButton
billId={record.id} billId={record.id}

View File

@@ -8,14 +8,16 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; 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 PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.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 PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -75,7 +77,11 @@ export function AccountingPayablesTableComponent({
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, 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: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -94,6 +100,9 @@ export function AccountingPayablesTableComponent({
title: t("payments.fields.amount"), title: t("payments.fields.amount"),
dataIndex: "amount", dataIndex: "amount",
key: "amount", key: "amount",
sorter: (a, b) => a.amount - b.amount,
sortOrder:
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<CurrencyFormatter>{record.amount}</CurrencyFormatter> <CurrencyFormatter>{record.amount}</CurrencyFormatter>
), ),
@@ -112,18 +121,21 @@ export function AccountingPayablesTableComponent({
title: t("payments.fields.created_at"), title: t("payments.fields.created_at"),
dataIndex: "created_at", dataIndex: "created_at",
key: "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) => ( render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter> <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
), ),
}, },
{ // {
title: t("payments.fields.exportedat"), // title: t("payments.fields.exportedat"),
dataIndex: "exportedat", // dataIndex: "exportedat",
key: "exportedat", // key: "exportedat",
render: (text, record) => ( // render: (text, record) => (
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter> // <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
), // ),
}, // },
{ {
title: t("exportlogs.labels.attempts"), title: t("exportlogs.labels.attempts"),
dataIndex: "attempts", dataIndex: "attempts",
@@ -137,8 +149,6 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
key: "actions", key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => ( render: (text, record) => (
<PaymentExportButton <PaymentExportButton
paymentId={record.id} paymentId={record.id}

View File

@@ -4,17 +4,19 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; 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 JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component"; import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter"; 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 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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -63,7 +65,7 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => a.status - b.status, sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
}, },
@@ -83,7 +85,8 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -103,6 +106,15 @@ export function AccountingReceivablesTableComponent({
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, 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) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}> <Link to={"/manage/vehicles/" + record.vehicleid}>

View File

@@ -12,8 +12,8 @@ import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton); export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
@@ -51,6 +51,7 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
insertAuditTrail({ insertAuditTrail({
jobid: jobid, jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number), operation: AuditTrailMapping.billdeleted(bill.invoice_number),
type: "billdeleted",
}); });
if (callback && typeof callback === "function") callback(bill.id); if (callback && typeof callback === "function") callback(bill.id);

View File

@@ -33,8 +33,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect( export default connect(
@@ -150,6 +150,7 @@ export function BillDetailEditcontainer({
jobid: bill.jobid, jobid: bill.jobid,
billid: search.billid, billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number), operation: AuditTrailMapping.billupdated(bill.invoice_number),
type: "billupdated",
}); });
await refetch(); await refetch();

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })), dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect( export default connect(

View File

@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, billid, operation }) => insertAuditTrail: ({ jobid, billid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, billid, operation })), dispatch(insertAuditTrail({ jobid, billid, operation, type })),
}); });
const Templates = TemplateList("job_special"); const Templates = TemplateList("job_special");
@@ -171,6 +171,7 @@ function BillEnterModalContainer({
mod_lbr_ty: key, mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1), hours: adjustmentsToInsert[key].toFixed(1),
}), }),
type: "jobmodifylbradj",
}); });
}); });
@@ -320,6 +321,7 @@ function BillEnterModalContainer({
operation: AuditTrailMapping.billposted( operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number r1.data.insert_bills.returning[0].invoice_number
), ),
type: "billposted",
}); });
if (enterAgain) { if (enterAgain) {

View File

@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")), toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
}); });
@@ -102,6 +102,7 @@ const CardPaymentModalComponent = ({
insertAuditTrail({ insertAuditTrail({
jobid: payment.jobid, jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(), operation: AuditTrailMapping.failedpayment(),
type: "failedpayment",
}) })
); );
}); });

View File

@@ -17,13 +17,18 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) { export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" },
}); });
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [filter, setFilter] = useLocalStorage(
"filter_courtesy_cars_list",
null
);
const { t } = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
@@ -50,6 +55,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
filteredValue: filter?.status || null,
filters: [ filters: [
{ {
text: t("courtesycars.status.in"), text: t("courtesycars.status.in"),
@@ -112,6 +118,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "readiness", dataIndex: "readiness",
key: "readiness", key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness), sorter: (a, b) => alphaSort(a.readiness, b.readiness),
filteredValue: filter?.readiness || null,
filters: [ filters: [
{ {
text: t("courtesycars.readiness.ready"), text: t("courtesycars.readiness.ready"),
@@ -227,7 +234,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, sortedInfo: sorter });
setFilter(filters);
}; };
const tableData = searchText const tableData = searchText

View File

@@ -0,0 +1,169 @@
import {Card, Table, Tag} from "antd";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import {useTranslation} from "react-i18next";
import React, {useEffect, useState} from "react";
import moment from "moment";
import DashboardRefreshRequired from "../refresh-required.component";
import axios from "axios";
const fortyFiveDaysAgo = () => moment().subtract(45, 'days').toLocaleString();
export default function JobLifecycleDashboardComponent({data, bodyshop, ...cardProps}) {
const {t} = useTranslation();
const [loading, setLoading] = useState(false);
const [lifecycleData, setLifecycleData] = useState(null);
useEffect(() => {
async function getLifecycleData() {
if (data && data.job_lifecycle) {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: data.job_lifecycle.map(x => x.id),
statuses: bodyshop.md_order_statuses
});
setLifecycleData(response.data.durations);
setLoading(false);
}
}
getLifecycleData().catch(e => {
console.error(`Error in getLifecycleData: ${e}`);
})
}, [data, bodyshop]);
const columns = [
{
title: t('job_lifecycle.columns.status'),
dataIndex: 'status',
bgColor: 'red',
key: 'status',
render: (text, record) => {
return <Tag color={record.color}>{record.status}</Tag>
}
},
{
title: t('job_lifecycle.columns.human_readable'),
dataIndex: 'humanReadable',
key: 'humanReadable',
},
{
title: t('job_lifecycle.columns.status_count'),
key: 'statusCount',
render: (text, record) => {
return lifecycleData.statusCounts[record.status];
}
},
{
title: t('job_lifecycle.columns.percentage'),
dataIndex: 'percentage',
key: 'percentage',
render: (text, record) => {
return record.percentage.toFixed(2) + '%';
}
},
];
if (!data) return null;
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}`
return (
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
<LoadingSkeleton loading={loading}>
<div style={{overflow: 'scroll', height: "100%"}}>
<div id="bar-container" style={{
display: 'flex',
width: '100%',
height: '100px',
textAlign: 'center',
borderRadius: '5px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: '#f0f2f5',
margin: 0,
padding: 0
}}>
{lifecycleData.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div key={key.status} style={{
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
padding: 0,
borderTop: '1px solid #f0f2f5',
borderBottom: '1px solid #f0f2f5',
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ?
<>
<div>{key.roundedPercentage}</div>
<div style={{
backgroundColor: '#f0f2f5',
borderRadius: '5px',
paddingRight: '2px',
paddingLeft: '2px',
fontSize: '0.8rem',
}}>
{key.status}
</div>
</>
: null}
</div>
);
})}
</div>
<Card extra={extra} type='inner' title={t('job_lifecycle.content.legend_title')}
style={{marginTop: '10px'}}>
<div>
{lifecycleData.summations.map((key) => (
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: '#f0f2f5',
color: '#000',
padding: '4px',
textAlign: 'center'
}}>
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
<Card style={{marginTop: "5px"}} type='inner' title={t("job_lifecycle.titles.top_durations")}>
<Table size="small" pagination={false} columns={columns}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}/>
</Card>
</div>
</LoadingSkeleton>
</Card>
);
}
export const JobLifecycleDashboardGQL = `
job_lifecycle: jobs(where: {
actual_in: {
_gte: "${moment().subtract(45, 'days').toISOString()}"
}
}) {
id
actual_in
} `;

View File

@@ -1,380 +1,391 @@
import Icon, { SyncOutlined } from "@ant-design/icons"; import Icon, {SyncOutlined} from "@ant-design/icons";
import { gql, useMutation, useQuery } from "@apollo/client"; import {gql, useMutation, useQuery} from "@apollo/client";
import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd"; import {Button, Dropdown, Menu, notification, PageHeader, Space} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import _ from "lodash"; import _ from "lodash";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, {useState} from "react";
import { Responsive, WidthProvider } from "react-grid-layout"; import {Responsive, WidthProvider} from "react-grid-layout";
import { useTranslation } from "react-i18next"; import {useTranslation} from "react-i18next";
import { MdClose } from "react-icons/md"; import {MdClose} from "react-icons/md";
import { connect } from "react-redux"; import {connect} from "react-redux";
import { createStructuredSelector } from "reselect"; import {createStructuredSelector} from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils"; import {logImEXEvent} from "../../firebase/firebase.utils";
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries"; import {UPDATE_DASHBOARD_LAYOUT} from "../../graphql/user.queries";
import { import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import DashboardMonthlyEmployeeEfficiency, { import DashboardMonthlyEmployeeEfficiency, {
DashboardMonthlyEmployeeEfficiencyGql, DashboardMonthlyEmployeeEfficiencyGql,
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component"; } from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component"; import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component"; import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component"; import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
import DashboardMonthlyRevenueGraph, { import DashboardMonthlyRevenueGraph, {
DashboardMonthlyRevenueGraphGql, DashboardMonthlyRevenueGraphGql,
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component"; } from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
import DashboardProjectedMonthlySales, { import DashboardProjectedMonthlySales, {
DashboardProjectedMonthlySalesGql, DashboardProjectedMonthlySalesGql,
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component"; } from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component"; import DashboardTotalProductionDollars
from "../dashboard-components/total-production-dollars/total-production-dollars.component";
import DashboardTotalProductionHours, { import DashboardTotalProductionHours, {
DashboardTotalProductionHoursGql, DashboardTotalProductionHoursGql,
} from "../dashboard-components/total-production-hours/total-production-hours.component"; } from "../dashboard-components/total-production-hours/total-production-hours.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
//Combination of the following: //Combination of the following:
// /node_modules/react-grid-layout/css/styles.css // /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css // /node_modules/react-resizable/css/styles.css
import DashboardScheduledInToday, { import DashboardScheduledInToday, {
DashboardScheduledInTodayGql, DashboardScheduledInTodayGql,
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component"; } from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
import DashboardScheduledOutToday, { import DashboardScheduledOutToday, {
DashboardScheduledOutTodayGql, DashboardScheduledOutTodayGql,
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component"; } from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
import JobLifecycleDashboardComponent, {
JobLifecycleDashboardGQL
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component";
import "./dashboard-grid.styles.scss"; import "./dashboard-grid.styles.scss";
import { GenerateDashboardData } from "./dashboard-grid.utils"; import {GenerateDashboardData} from "./dashboard-grid.utils";
const ResponsiveReactGridLayout = WidthProvider(Responsive); const ResponsiveReactGridLayout = WidthProvider(Responsive);
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export function DashboardGridComponent({ currentUser, bodyshop }) { export function DashboardGridComponent({currentUser, bodyshop}) {
const { t } = useTranslation(); const {t} = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout ...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout ? bodyshop.associations[0].user.dashboardlayout
: { items: [], layout: {}, layouts: [] }), : {items: [], layout: {}, layouts: []}),
});
const { loading, error, data, refetch } = useQuery(
createDashboardQuery(state),
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
);
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const handleLayoutChange = async (layout, layouts) => {
logImEXEvent("dashboard_change_layout");
setState({ ...state, layout, layouts });
const result = await updateLayout({
variables: {
email: currentUser.email,
layout: { ...state, layout, layouts },
},
}); });
if (!!result.errors) {
notification["error"]({
message: t("dashboard.errors.updatinglayout", {
message: JSON.stringify(result.errors),
}),
});
}
};
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", { name: key });
const idxToRemove = state.items.findIndex((i) => i.i === key);
const items = _.cloneDeep(state.items); const {loading, error, data, refetch} = useQuery(
createDashboardQuery(state),
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
);
items.splice(idxToRemove, 1); const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
setState({ ...state, items });
};
const handleAddComponent = (e) => { const handleLayoutChange = async (layout, layouts) => {
logImEXEvent("dashboard_add_component", { name: e }); logImEXEvent("dashboard_change_layout");
setState({
...state,
items: [
...state.items,
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
],
});
};
const dashboarddata = React.useMemo( setState({...state, layout, layouts});
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
if (error) return <AlertComponent message={error.message} type="error" />; const result = await updateLayout({
variables: {
return ( email: currentUser.email,
<div> layout: {...state, layout, layouts},
<PageHeader },
extra={ });
<Space> if (!!result.errors) {
<Button onClick={() => refetch()}> notification["error"]({
<SyncOutlined /> message: t("dashboard.errors.updatinglayout", {
</Button> message: JSON.stringify(result.errors),
<Dropdown overlay={addComponentOverlay} trigger={["click"]}> }),
<Button>{t("dashboard.actions.addcomponent")}</Button> });
</Dropdown>
</Space>
} }
/> };
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", {name: key});
const idxToRemove = state.items.findIndex((i) => i.i === key);
<ResponsiveReactGridLayout const items = _.cloneDeep(state.items);
className="layout"
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} items.splice(idxToRemove, 1);
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} setState({...state, items});
width="100%" };
layouts={state.layouts}
onLayoutChange={handleLayoutChange} const handleAddComponent = (e) => {
// onBreakpointChange={onBreakpointChange} logImEXEvent("dashboard_add_component", {name: e});
> setState({
{state.items.map((item, index) => { ...state,
const TheComponent = componentList[item.i].component; items: [
return ( ...state.items,
<div {
key={item.i} i: e.key,
data-grid={{ x: (state.items.length * 2) % (state.cols || 12),
...item, y: 99, // puts it at the bottom
minH: componentList[item.i].minH || 1, w: componentList[e.key].w || 2,
minW: componentList[item.i].minW || 1, h: componentList[e.key].h || 2,
}} },
],
});
};
const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
if (error) return <AlertComponent message={error.message} type="error"/>;
return (
<div>
<PageHeader
extra={
<Space>
<Button onClick={() => refetch()}>
<SyncOutlined/>
</Button>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
}
/>
<ResponsiveReactGridLayout
className="layout"
breakpoints={{lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}}
cols={{lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
> >
<LoadingSkeleton loading={loading}> {state.items.map((item, index) => {
<Icon const TheComponent = componentList[item.i].component;
component={MdClose} return (
key={item.i} <div
style={{ key={item.i}
position: "absolute", data-grid={{
zIndex: "2", ...item,
right: ".25rem", minH: componentList[item.i].minH || 1,
top: ".25rem", minW: componentList[item.i].minW || 1,
cursor: "pointer", }}
}} >
onClick={() => handleRemoveComponent(item.i)} <LoadingSkeleton loading={loading}>
/> <Icon
<TheComponent className="dashboard-card" data={dashboarddata} /> component={MdClose}
</LoadingSkeleton> key={item.i}
</div> style={{
); position: "absolute",
})} zIndex: "2",
</ResponsiveReactGridLayout> right: ".25rem",
</div> top: ".25rem",
); cursor: "pointer",
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboarddata}/>
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</div>
);
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(DashboardGridComponent); )(DashboardGridComponent);
const componentList = { const componentList = {
ProductionDollars: { ProductionDollars: {
label: i18next.t("dashboard.titles.productiondollars"), label: i18next.t("dashboard.titles.productiondollars"),
component: DashboardTotalProductionDollars, component: DashboardTotalProductionDollars,
gqlFragment: null, gqlFragment: null,
w: 1, w: 1,
h: 1, h: 1,
minW: 2, minW: 2,
minH: 1, minH: 1,
}, },
ProductionHours: { ProductionHours: {
label: i18next.t("dashboard.titles.productionhours"), label: i18next.t("dashboard.titles.productionhours"),
component: DashboardTotalProductionHours, component: DashboardTotalProductionHours,
gqlFragment: DashboardTotalProductionHoursGql, gqlFragment: DashboardTotalProductionHoursGql,
w: 3, w: 3,
h: 1, h: 1,
minW: 3, minW: 3,
minH: 1, minH: 1,
}, },
ProjectedMonthlySales: { ProjectedMonthlySales: {
label: i18next.t("dashboard.titles.projectedmonthlysales"), label: i18next.t("dashboard.titles.projectedmonthlysales"),
component: DashboardProjectedMonthlySales, component: DashboardProjectedMonthlySales,
gqlFragment: DashboardProjectedMonthlySalesGql, gqlFragment: DashboardProjectedMonthlySalesGql,
w: 2, w: 2,
h: 1, h: 1,
minW: 2, minW: 2,
minH: 1, minH: 1,
}, },
MonthlyRevenueGraph: { MonthlyRevenueGraph: {
label: i18next.t("dashboard.titles.monthlyrevenuegraph"), label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
component: DashboardMonthlyRevenueGraph, component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql, gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 4, w: 4,
h: 2, h: 2,
minW: 4, minW: 4,
minH: 2, minH: 2,
}, },
MonthlyJobCosting: { MonthlyJobCosting: {
label: i18next.t("dashboard.titles.monthlyjobcosting"), label: i18next.t("dashboard.titles.monthlyjobcosting"),
component: DashboardMonthlyJobCosting, component: DashboardMonthlyJobCosting,
gqlFragment: null, gqlFragment: null,
minW: 6, minW: 6,
minH: 3, minH: 3,
w: 6, w: 6,
h: 3, h: 3,
}, },
MonthlyPartsSales: { MonthlyPartsSales: {
label: i18next.t("dashboard.titles.monthlypartssales"), label: i18next.t("dashboard.titles.monthlypartssales"),
component: DashboardMonthlyPartsSales, component: DashboardMonthlyPartsSales,
gqlFragment: null, gqlFragment: null,
minW: 2, minW: 2,
minH: 2, minH: 2,
w: 2, w: 2,
h: 2, h: 2,
}, },
MonthlyLaborSales: { MonthlyLaborSales: {
label: i18next.t("dashboard.titles.monthlylaborsales"), label: i18next.t("dashboard.titles.monthlylaborsales"),
component: DashboardMonthlyLaborSales, component: DashboardMonthlyLaborSales,
gqlFragment: null, gqlFragment: null,
minW: 2, minW: 2,
minH: 2, minH: 2,
w: 2, w: 2,
h: 2, h: 2,
}, },
MonthlyEmployeeEfficency: { // Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"), MonthlyEmployeeEfficency: {
component: DashboardMonthlyEmployeeEfficiency, label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql, component: DashboardMonthlyEmployeeEfficiency,
minW: 2, gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
minH: 2, minW: 2,
w: 2, minH: 2,
h: 2, w: 2,
}, h: 2,
ScheduleInToday: { },
label: i18next.t("dashboard.titles.scheduledintoday"), ScheduleInToday: {
component: DashboardScheduledInToday, label: i18next.t("dashboard.titles.scheduledintoday"),
gqlFragment: DashboardScheduledInTodayGql, component: DashboardScheduledInToday,
minW: 6, gqlFragment: DashboardScheduledInTodayGql,
minH: 2, minW: 6,
w: 10, minH: 2,
h: 3, w: 10,
}, h: 3,
ScheduleOutToday: { },
label: i18next.t("dashboard.titles.scheduledouttoday"), ScheduleOutToday: {
component: DashboardScheduledOutToday, label: i18next.t("dashboard.titles.scheduledouttoday"),
gqlFragment: DashboardScheduledOutTodayGql, component: DashboardScheduledOutToday,
minW: 6, gqlFragment: DashboardScheduledOutTodayGql,
minH: 2, minW: 6,
w: 10, minH: 2,
h: 3, w: 10,
}, h: 3,
},
JobLifecycle: {
label: i18next.t("dashboard.titles.joblifecycle"),
component: JobLifecycleDashboardComponent,
gqlFragment: JobLifecycleDashboardGQL,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
}; };
const createDashboardQuery = (state) => { const createDashboardQuery = (state) => {
const componentBasedAdditions = const componentBasedAdditions =
state && state &&
Array.isArray(state.layout) && Array.isArray(state.layout) &&
state.layout state.layout
.map((item, index) => componentList[item.i].gqlFragment || "") .map((item, index) => componentList[item.i].gqlFragment || "")
.join(""); .join("");
return gql` return gql`
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""} query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [ monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}}, { voided: {_eq: false}},
{date_invoiced: {_gte: "${moment() {date_invoiced: {_gte: "${moment()
.startOf("month") .startOf("month")
.startOf("day") .startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${moment() .toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month") .endOf("month")
.endOf("day") .endOf("day")
.toISOString()}"}}]}) { .toISOString()}"}}]}) {
id id
ro_number ro_number
date_invoiced date_invoiced
job_totals job_totals
rate_la1 rate_la1
rate_la2 rate_la2
rate_la3 rate_la3
rate_la4 rate_la4
rate_laa rate_laa
rate_lab rate_lab
rate_lad rate_lad
rate_lae rate_lae
rate_laf rate_laf
rate_lag rate_lag
rate_lam rate_lam
rate_lar rate_lar
rate_las rate_las
rate_lau rate_lau
rate_ma2s rate_ma2s
rate_ma2t rate_ma2t
rate_ma3s rate_ma3s
rate_mabl rate_mabl
rate_macs rate_macs
rate_mahw rate_mahw
rate_mapa rate_mapa
rate_mash rate_mash
rate_matd rate_matd
joblines(where: { removed: { _eq: false } }) { joblines(where: { removed: { _eq: false } }) {
id id
mod_lbr_ty mod_lbr_ty
mod_lb_hrs mod_lb_hrs
act_price act_price
part_qty part_qty
part_type part_type
} }
} }
production_jobs: jobs(where: { inproduction: { _eq: true } }) { production_jobs: jobs(where: { inproduction: { _eq: true } }) {
id id
ro_number ro_number
ins_co_nm ins_co_nm
job_totals job_totals
joblines(where: { removed: { _eq: false } }) { joblines(where: { removed: { _eq: false } }) {
id id
mod_lbr_ty mod_lbr_ty
mod_lb_hrs mod_lb_hrs
act_price act_price
part_qty part_qty
part_type part_type
} }
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) { labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
} }
} }
} }
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) { larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
aggregate { aggregate {
sum { sum {
mod_lb_hrs mod_lb_hrs
} }
} }
} }
} }
}`; }`;
}; };

View File

@@ -53,6 +53,7 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
insertAuditTrail({ insertAuditTrail({
jobid: event.job.id, jobid: event.job.id,
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason), operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel",
}) })
); );
} }

View File

@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function JobChecklistForm({ export function JobChecklistForm({
@@ -183,6 +183,7 @@ export function JobChecklistForm({
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) || (type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered) (type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
), ),
type: "jobchecklist",
}); });
} else { } else {
notification["error"]({ notification["error"]({

View File

@@ -18,10 +18,8 @@ export default function JobDetailCardsTotalsComponent({ loading, data }) {
/> />
<Statistic <Statistic
className="imex-flex-row__margin-large" className="imex-flex-row__margin-large"
title={t("jobs.fields.ded_amt")} title={t("jobs.fields.customerowing")}
value={Dinero({ value={Dinero(data.job_totals.totals.custPayable.total).toFormat()}
amount: Math.round((data.ded_amt || 0) * 100),
}).toFormat()}
/> />
<Statistic <Statistic
className="imex-flex-row__margin-large" className="imex-flex-row__margin-large"

View File

@@ -23,7 +23,6 @@ export function JobEmployeeAssignments({
jobRO, jobRO,
body, body,
refinish, refinish,
prep, prep,
csr, csr,
handleAdd, handleAdd,
@@ -78,7 +77,7 @@ export function JobEmployeeAssignments({
setVisibility(false); setVisibility(false);
}} }}
> >
Assign {t("allocations.actions.assign")}
</Button> </Button>
<Button onClick={() => setVisibility(false)}>Close</Button> <Button onClick={() => setVisibility(false)}>Close</Button>
</Space> </Space>

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -43,12 +43,13 @@ export function JobEmployeeAssignmentsContainer({
}); });
if (refetch) refetch(); if (refetch) refetch();
insertAuditTrail({ if (!!!result.errors) {
jobid: job.id, insertAuditTrail({
operation: AuditTrailMapping.jobassignmentchange(operation, name), jobid: job.id,
}); operation: AuditTrailMapping.jobassignmentchange(operation, name),
type: "jobassignmentchange",
if (!!result.errors) { });
} else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.assigning", { message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors), message: JSON.stringify(result.errors),
@@ -66,17 +67,19 @@ export function JobEmployeeAssignmentsContainer({
variables: { jobId: job.id, job: { [empAssignment]: null } }, variables: { jobId: job.id, job: { [empAssignment]: null } },
}); });
if (!!result.errors) { if (!!!result.errors) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
type: "jobassignmentremoved",
});
} else {
notification["error"]({ notification["error"]({
message: t("jobs.errors.assigning", { message: t("jobs.errors.assigning", {
message: JSON.stringify(result.errors), message: JSON.stringify(result.errors),
}), }),
}); });
} }
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
});
setLoading(false); setLoading(false);
}; };

View File

@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -113,6 +113,7 @@ export function JobLineConvertToLabor({
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1), hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
mod_lbr_ty, mod_lbr_ty,
}), }),
type: "jobmodifylbradj",
}); });
setLoading(false); setLoading(false);
setVisibility(false); setVisibility(false);

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
@@ -32,6 +32,7 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobstatuschange(status), operation: AuditTrailMapping.admin_jobstatuschange(status),
type: "admin_jobstatuschange",
}); });
// refetch(); // refetch();
}) })

View File

@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect( export default connect(
@@ -57,6 +57,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
? DateTimeFormat(changedAuditFields[key]) ? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key] : changedAuditFields[key]
), ),
type: "admin_jobfieldchange",
}); });
}); });

View File

@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -59,6 +59,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkforreexport(), operation: AuditTrailMapping.admin_jobmarkforreexport(),
type: "admin_jobmarkforreexport",
}); });
} else { } else {
notification["error"]({ notification["error"]({
@@ -99,6 +100,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(), operation: AuditTrailMapping.admin_jobmarkexported(),
type: "admin_jobmarkexported",
}); });
} else { } else {
notification["error"]({ notification["error"]({
@@ -124,6 +126,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobuninvoice(), operation: AuditTrailMapping.admin_jobuninvoice(),
type: "admin_jobuninvoice",
}); });
} else { } else {
notification["error"]({ notification["error"]({

View File

@@ -10,8 +10,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
@@ -34,6 +34,7 @@ export function JobsAdminRemoveAR({ insertAuditTrail, job }) {
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_job_remove_from_ar(value), operation: AuditTrailMapping.admin_job_remove_from_ar(value),
type: "admin_job_remove_from_ar",
}); });
setSwitchValue(value); setSwitchValue(value);
} else { } else {

View File

@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid); export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
@@ -49,6 +49,7 @@ export function JobsAdminUnvoid({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.admin_jobunvoid(), operation: AuditTrailMapping.admin_jobunvoid(),
type: "admin_jobunvoid",
}); });
} else { } else {
notification["error"]({ notification["error"]({

View File

@@ -47,8 +47,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function JobsAvailableContainer({ export function JobsAvailableContainer({
bodyshop, bodyshop,
@@ -190,6 +190,7 @@ export function JobsAvailableContainer({
insertAuditTrail({ insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id, jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(), operation: AuditTrailMapping.jobimported(),
type: "jobimported",
}); });
deleteJob({ deleteJob({
@@ -350,6 +351,7 @@ export function JobsAvailableContainer({
insertAuditTrail({ insertAuditTrail({
jobid: selectedJob, jobid: selectedJob,
operation: AuditTrailMapping.jobsupplement(), operation: AuditTrailMapping.jobsupplement(),
type: "jobsupplement",
}); });
} }
}; };

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) { export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
@@ -35,6 +35,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.jobstatuschange(status), operation: AuditTrailMapping.jobstatuschange(status),
type: "jobstatuschange",
}); });
// refetch(); // refetch();
}) })

View File

@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
function updateJobCache(items) { function updateJobCache(items) {
@@ -192,6 +192,7 @@ export function JobsCloseExportButton({
insertAuditTrail({ insertAuditTrail({
jobid: jobId, jobid: jobId,
operation: AuditTrailMapping.jobexported(), operation: AuditTrailMapping.jobexported(),
type: "jobexported",
}); });
updateJobCache( updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id) jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
@@ -217,6 +218,7 @@ export function JobsCloseExportButton({
insertAuditTrail({ insertAuditTrail({
jobid: jobId, jobid: jobId,
operation: AuditTrailMapping.jobexported(), operation: AuditTrailMapping.jobexported(),
type: "jobexported",
}); });
updateJobCache([ updateJobCache([
...new Set( ...new Set(

View File

@@ -25,8 +25,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function JobsConvertButton({ export function JobsConvertButton({
@@ -78,6 +78,7 @@ export function JobsConvertButton({
operation: AuditTrailMapping.jobconverted( operation: AuditTrailMapping.jobconverted(
res.data.update_jobs.returning[0].ro_number res.data.update_jobs.returning[0].ro_number
), ),
type: "jobconverted",
}); });
setVisible(false); setVisible(false);

View File

@@ -29,6 +29,7 @@ export default function AddToProduction(
insertAuditTrail({ insertAuditTrail({
jobid: jobId, jobid: jobId,
operation: AuditTrailMapping.jobinproductionchange(!remove), operation: AuditTrailMapping.jobinproductionchange(!remove),
type: "jobinproductionchange",
}) })
); );
if (completionCallback) completionCallback(); if (completionCallback) completionCallback();

View File

@@ -52,8 +52,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "timeTicket" })), dispatch(setModalContext({ context: context, modal: "timeTicket" })),
setCardPaymentContext: (context) => setCardPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "cardPayment" })), dispatch(setModalContext({ context: context, modal: "cardPayment" })),
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function JobsDetailHeaderActions({ export function JobsDetailHeaderActions({
@@ -115,6 +115,7 @@ export function JobsDetailHeaderActions({
? !job.production_vars.alert ? !job.production_vars.alert
: true : true
), ),
type: "alertToggle",
}); });
}; };
@@ -134,6 +135,7 @@ export function JobsDetailHeaderActions({
operation: AuditTrailMapping.jobsuspend( operation: AuditTrailMapping.jobsuspend(
!!job.suspended ? !job.suspended : true !!job.suspended ? !job.suspended : true
), ),
type: "jobsuspend",
}); });
}; };
@@ -190,6 +192,7 @@ export function JobsDetailHeaderActions({
jobid: job.id, jobid: job.id,
operation: operation:
AuditTrailMapping.appointmentcancel(lost_sale_reason), AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel",
}); });
return; return;
} }
@@ -549,6 +552,7 @@ export function JobsDetailHeaderActions({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.jobvoid(), operation: AuditTrailMapping.jobvoid(),
type: "jobvoid",
}); });
//go back to jobs list. //go back to jobs list.
history.push(`/manage/`); history.push(`/manage/`);

View File

@@ -4,7 +4,7 @@ import {
PauseCircleOutlined, PauseCircleOutlined,
WarningFilled, WarningFilled,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Col, Row, Space, Tag, Tooltip } from "antd"; import { Card, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -26,6 +26,7 @@ import ProductionListColumnComment from "../production-list-columns/production-l
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component"; import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import "./jobs-detail-header.styles.scss"; import "./jobs-detail-header.styles.scss";
import moment from "moment";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -62,6 +63,13 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
${job.v_make_desc || ""} ${job.v_make_desc || ""}
${job.v_model_desc || ""}`.trim(); ${job.v_model_desc || ""}`.trim();
const bodyHrs = job.joblines
.filter((j) => j.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const refinishHrs = job.joblines
.filter((line) => line.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0);
const ownerTitle = OwnerNameDisplayFunction(job).trim(); const ownerTitle = OwnerNameDisplayFunction(job).trim();
return ( return (
@@ -93,7 +101,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{job.status === bodyshop.md_ro_statuses.default_scheduled && {job.status === bodyshop.md_ro_statuses.default_scheduled &&
job.scheduled_in ? ( job.scheduled_in ? (
<Tag> <Tag>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter> <Link to={`/manage/schedule?date=${moment(job.scheduled_in).format('YYYY-MM-DD')}`}>
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
</Link>
</Tag> </Tag>
) : null} ) : null}
</Space> </Space>
@@ -299,6 +309,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
> >
<div> <div>
<JobEmployeeAssignments job={job} /> <JobEmployeeAssignments job={job} />
<Divider style={{ margin: ".5rem" }} />
<DataLabel label={t("jobs.labels.labor_hrs")}>
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} /{" "}
{(bodyHrs + refinishHrs).toFixed(1)}
</DataLabel>
</div> </div>
</Card> </Card>
</Col> </Col>

View File

@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
function updateJobCache(items) { function updateJobCache(items) {
@@ -189,6 +189,7 @@ export function JobsExportAllButton({
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.jobexported(), operation: AuditTrailMapping.jobexported(),
type: "jobexported",
}); });
}); });
updateJobCache( updateJobCache(
@@ -230,6 +231,7 @@ export function JobsExportAllButton({
insertAuditTrail({ insertAuditTrail({
jobid: successfulTransactionsSet[0], jobid: successfulTransactionsSet[0],
operation: AuditTrailMapping.jobexported(), operation: AuditTrailMapping.jobexported(),
type: "jobexported",
}); });
} }
updateJobCache(successfulTransactionsSet); updateJobCache(successfulTransactionsSet);

View File

@@ -21,6 +21,7 @@ import useLocalStorage from "../../utils/useLocalStorage";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -145,7 +146,8 @@ export function JobsList({ bodyshop }) {
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
@@ -188,7 +190,8 @@ export function JobsList({ bodyshop }) {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
ellipsis: true, 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: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: filter?.status || null, filteredValue: filter?.status || null,
@@ -219,6 +222,15 @@ export function JobsList({ bodyshop }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, 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) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -266,6 +278,9 @@ export function JobsList({ bodyshop }) {
dataIndex: "ins_co_nm", dataIndex: "ins_co_nm",
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, 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, filteredValue: filter?.ins_co_nm || null,
filters: filters:
(jobs && (jobs &&
@@ -302,6 +317,13 @@ export function JobsList({ bodyshop }) {
key: "estimator", key: "estimator",
ellipsis: true, ellipsis: true,
responsive: ["xl"], 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, filterSearch: true,
filteredValue: filter?.estimator || null, filteredValue: filter?.estimator || null,
filters: filters:

View File

@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer); export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer);
@@ -49,6 +49,7 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
insertAuditTrail({ insertAuditTrail({
jobid: jobId, jobid: jobId,
operation: AuditTrailMapping.jobnotedeleted(), operation: AuditTrailMapping.jobnotedeleted(),
type: "jobnotedeleted",
}); });
}); });
setDeleteLoading(false); setDeleteLoading(false);

View File

@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
@@ -76,6 +76,7 @@ export function LaborAllocationsAdjustmentEdit({
values.hours - values.hours -
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1), ((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
}), }),
type: "jobmodifylbradj",
}); });
} }
setLoading(false); setLoading(false);

View File

@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")), toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function NoteUpsertModalContainer({ export function NoteUpsertModalContainer({
@@ -70,6 +70,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({ insertAuditTrail({
jobid: context.jobId, jobid: context.jobId,
operation: AuditTrailMapping.jobnoteupdated(), operation: AuditTrailMapping.jobnoteupdated(),
type: "jobnoteupdated",
}); });
}); });
if (refetch) refetch(); if (refetch) refetch();
@@ -102,6 +103,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({ insertAuditTrail({
jobid: newJobId, jobid: newJobId,
operation: AuditTrailMapping.jobnoteadded(), operation: AuditTrailMapping.jobnoteadded(),
type: "jobnoteadded",
}); });
}); });
} }
@@ -115,6 +117,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({ insertAuditTrail({
jobid: context.jobId, jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(), operation: AuditTrailMapping.jobnoteadded(),
type: "jobnoteadded",
}); });
} }
}; };

View File

@@ -44,6 +44,15 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.vehicle"), title: t("jobs.fields.vehicle"),
dataIndex: "vehicleid", dataIndex: "vehicleid",
key: "vehicleid", key: "vehicleid",
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 === "vehicleid" && state.sortedInfo.order,
render: (text, record) => render: (text, record) =>
record.vehicleid ? ( record.vehicleid ? (
<Link to={`/manage/vehicles/${record.vehicleid}`}> <Link to={`/manage/vehicles/${record.vehicleid}`}>
@@ -67,9 +76,15 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses), sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
text: status,
value: status,
})),
onFilter: (value, record) => value.includes(record.status),
}, },
{ {

View File

@@ -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 { Form, Modal, notification } from "antd";
import axios from "axios";
import _ from "lodash";
import moment from "moment"; import moment from "moment";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
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_LINE_STATUS } from "../../graphql/jobs-lines.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { import {
INSERT_NEW_PARTS_ORDERS, INSERT_NEW_PARTS_ORDERS,
QUERY_PARTS_ORDER_OEC, QUERY_PARTS_ORDER_OEC,
@@ -29,10 +33,6 @@ import { TemplateList } from "../../utils/TemplateConstants";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import PartsOrderModalComponent from "./parts-order-modal.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({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -45,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")), toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")),
setBillEnterContext: (context) => setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })), dispatch(setModalContext({ context: context, modal: "billEnter" })),
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function PartsOrderModalContainer({ export function PartsOrderModalContainer({
@@ -142,6 +142,7 @@ export function PartsOrderModalContainer({
: AuditTrailMapping.jobspartsorder( : AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number insertResult.data.insert_parts_orders.returning[0].order_number
), ),
type: isReturn ? "jobspartsreturn" : "jobspartsorder",
}); });
const jobLinesResult = await updateJobLines({ const jobLinesResult = await updateJobLines({

View File

@@ -95,15 +95,13 @@ export function PartsQueueListComponent({ bodyshop }) {
}; };
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
if (record) { if (record?.id) {
if (record.id) { history.replace({
history.push({ search: queryString.stringify({
search: queryString.stringify({ ...searchParams,
...searchParams, selected: record.id,
selected: record.id, }),
}), });
});
}
} }
}; };
@@ -350,6 +348,13 @@ export function PartsQueueListComponent({ bodyshop }) {
selectedRowKeys: [selected], selectedRowKeys: [selected],
type: "radio", type: "radio",
}} }}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/> />
</Card> </Card>
); );

View File

@@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
await insertPayment({ await insertPayment({
variables: { variables: {
paymentInput: { paymentInput: {
amount: -refund_response.data.amount, amount: -refund_response?.data?.amount,
transactionid: payment_response.response.receiptelements.transid, transactionid: payment_response?.response?.receiptelements?.transid,
payer: record.payer, payer: record.payer,
type: "Refund", type: "Refund",
jobid: payment_response.jobid, jobid: payment_response.jobid,

View File

@@ -1,6 +1,12 @@
import { Button, Form, Input, PageHeader, Space } from "antd"; import { Button, Form, Input, PageHeader, Space } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; 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 FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { import PhoneFormItem, {
@@ -8,12 +14,6 @@ import PhoneFormItem, {
} from "../form-items-formatted/phone-form-item.component"; } from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.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({ const mapStateToProps = createStructuredSelector({
authLevel: selectAuthLevel, authLevel: selectAuthLevel,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -46,13 +46,19 @@ export function PhonebookFormComponent({
return ( return (
<div> <div>
<PageHeader <PageHeader
title={`${form.getFieldValue("firstname") || ""} ${ title={
form.getFieldValue("lastname") || "" <Form.Item shouldUpdate>
}${ {() =>
form.getFieldValue("company") `${form.getFieldValue("firstname") || ""} ${
? ` - ${form.getFieldValue("company")}` form.getFieldValue("lastname") || ""
: "" }${
}`} form.getFieldValue("company")
? ` - ${form.getFieldValue("company")}`
: ""
}`
}
</Form.Item>
}
extra={ extra={
<Space> <Space>
<Button <Button

View File

@@ -29,8 +29,8 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function ProductionBoardKanbanComponent({ export function ProductionBoardKanbanComponent({
@@ -133,6 +133,7 @@ export function ProductionBoardKanbanComponent({
insertAuditTrail({ insertAuditTrail({
jobid: card.id, jobid: card.id,
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId), operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
type: "jobstatuschange",
}); });
if (update.errors) { if (update.errors) {

View File

@@ -13,8 +13,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({}); const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function ProductionListColumnAlert({ record, insertAuditTrail }) { export function ProductionListColumnAlert({ record, insertAuditTrail }) {
@@ -46,6 +46,7 @@ export function ProductionListColumnAlert({ record, insertAuditTrail }) {
? !record.production_vars.alert ? !record.production_vars.alert
: true : true
), ),
type: "alertToggle",
}).then(() => { }).then(() => {
if (record.refetch) record.refetch(); if (record.refetch) record.refetch();
}); });

View File

@@ -1,7 +1,6 @@
import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons"; import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons";
import { Space, Tooltip } from "antd"; import { Checkbox, Space, Tooltip } from "antd";
import i18n from "i18next"; import i18n from "i18next";
import moment from "moment";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { TimeFormatter } from "../../utils/DateFormatter"; import { TimeFormatter } from "../../utils/DateFormatter";
@@ -10,7 +9,9 @@ import { onlyUnique } from "../../utils/arrayHelper";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
import JobAltTransportChange from "../job-at-change/job-at-change.component"; import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.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 ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import ProductionListColumnAlert from "./production-list-columns.alert.component"; import ProductionListColumnAlert from "./production-list-columns.alert.component";
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component"; import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
@@ -84,7 +85,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} />
</Link> </Link>
), ),
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order, state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
}, },
@@ -95,8 +97,10 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
ellipsis: true, ellipsis: true,
sorter: (a, b) => sorter: (a, b) =>
alphaSort( alphaSort(
a.v_make_desc + a.v_model_desc, `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
b.v_make_desc + b.v_model_desc a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
), ),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
@@ -185,17 +189,12 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
state.sortedInfo.columnKey === "date_next_contact" && state.sortedInfo.columnKey === "date_next_contact" &&
state.sortedInfo.order, state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<span <ProductionListDate
style={{ record={record}
color: field="date_next_contact"
record.date_next_contact && pastIndicator
moment(record.date_next_contact).isBefore(moment()) time
? "red" />
: "",
}}
>
<ProductionListDate record={record} field="date_next_contact" time />
</span>
), ),
}, },
{ {
@@ -291,6 +290,20 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
dataIndex: "special_coverage_policy", dataIndex: "special_coverage_policy",
key: "special_coverage_policy", key: "special_coverage_policy",
ellipsis: true, 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 +315,16 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
sortOrder: sortOrder:
state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order, 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) => ( render: (text, record) => (
<div> <div>
{record.alt_transport} {record.alt_transport}
@@ -382,7 +405,11 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
title: i18n.t("production.labels.alert"), title: i18n.t("production.labels.alert"),
dataIndex: "alert", dataIndex: "alert",
key: "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} />, render: (text, record) => <ProductionListColumnAlert record={record} />,
}, },
{ {

View File

@@ -3,12 +3,12 @@ import { useMutation } from "@apollo/client";
import { import {
Button, Button,
Col, Col,
notification,
Popover, Popover,
Row, Row,
Select, Select,
Space, Space,
Spin, Spin,
notification,
} from "antd"; } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -25,8 +25,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function ProductionListEmpAssignment({ export function ProductionListEmpAssignment({
@@ -55,6 +55,7 @@ export function ProductionListEmpAssignment({
insertAuditTrail({ insertAuditTrail({
jobid: record.id, jobid: record.id,
operation: AuditTrailMapping.jobassignmentchange(empAssignment, name), operation: AuditTrailMapping.jobassignmentchange(empAssignment, name),
type: "jobassignmentchange",
}); });
if (!!result.errors) { if (!!result.errors) {
@@ -80,6 +81,7 @@ export function ProductionListEmpAssignment({
insertAuditTrail({ insertAuditTrail({
jobid: record.id, jobid: record.id,
operation: AuditTrailMapping.jobassignmentremoved(empAssignment), operation: AuditTrailMapping.jobassignmentremoved(empAssignment),
type: "jobassignmentremoved",
}); });
if (!!result.errors) { if (!!result.errors) {

View File

@@ -12,8 +12,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function ProductionListColumnCategory({ record, bodyshop }) { export function ProductionListColumnCategory({ record, bodyshop }) {
const [updateJob] = useMutation(UPDATE_JOB); const [updateJob] = useMutation(UPDATE_JOB);

View File

@@ -5,16 +5,16 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function ProductionListColumnStatus({ export function ProductionListColumnStatus({
record, record,
@@ -40,6 +40,7 @@ export function ProductionListColumnStatus({
insertAuditTrail({ insertAuditTrail({
jobid: record.id, jobid: record.id,
operation: AuditTrailMapping.jobstatuschange(key), operation: AuditTrailMapping.jobstatuschange(key),
type: "jobstatuschange",
}); });
setLoading(false); setLoading(false);

View File

@@ -6,7 +6,7 @@ import {useTranslation} from "react-i18next";
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier"; import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import {generateInternalReflections} from "./report-center-modal-utils"; import {generateInternalReflections} from "./report-center-modal-utils";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) { export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
return ( return (
@@ -33,7 +33,7 @@ function FiltersSection({filters, form, bodyshop}) {
return ( return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}> <Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
<Form.List name={["filters"]}> <Form.List name={["filters"]}>
{(fields, {add, remove, move}) => { {(fields, {add, remove}) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -70,7 +70,9 @@ function FiltersSection({filters, form, bodyshop}) {
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.Item dependencies={[['filters', field.name, "field"]]}> <Form.Item
dependencies={[['filters', field.name, "field"],['filters', field.name, "value"]]}
>
{ {
() => { () => {
const name = form.getFieldValue(['filters', field.name, "field"]); const name = form.getFieldValue(['filters', field.name, "field"]);
@@ -80,7 +82,6 @@ function FiltersSection({filters, form, bodyshop}) {
key={`${index}operator`} key={`${index}operator`}
label={t('reportcenter.labels.advanced_filters_filter_operator')} label={t('reportcenter.labels.advanced_filters_filter_operator')}
name={[field.name, "operator"]} name={[field.name, "operator"]}
dependencies={[]}
rules={[ rules={[
{ {
required: true, required: true,
@@ -90,19 +91,32 @@ function FiltersSection({filters, form, bodyshop}) {
> >
<Select <Select
getPopupContainer={trigger => trigger.parentNode} getPopupContainer={trigger => trigger.parentNode}
options={getWhereOperatorsByType(type)}/> options={ getWhereOperatorsByType(type)}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], undefined);
}}
/>
</Form.Item> </Form.Item>
} }
} }
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.Item dependencies={[['filters', field.name, "field"]]}> <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 name = form.getFieldValue(['filters', field.name, "field"]);
const type = filters.find(f => f.name === name)?.type; const type = filters.find(f => f.name === name)?.type;
const reflector = filters.find(f => f.name === name)?.reflector; 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 return <Form.Item
key={`${index}value`} key={`${index}value`}
@@ -128,14 +142,29 @@ function FiltersSection({filters, form, bodyshop}) {
return generateInternalReflections({ return generateInternalReflections({
bodyshop, bodyshop,
upperPath, upperPath,
finalPath finalPath,
t
}); });
}; };
const reflections = reflector ? generateReflections(reflector) : []; const reflections = reflector ? generateReflections(reflector) : [];
const fieldPath = [[field.name, "value"]]; const fieldPath = [[field.name, "value"]];
// We have reflections so we will use a select box
if (reflections.length > 0) { 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 ( return (
<Select <Select
options={reflections} options={reflections}
@@ -147,16 +176,50 @@ function FiltersSection({filters, form, bodyshop}) {
); );
} }
// We have a type of number, so we will use a number input
if (type === "number") { if (type === "number") {
return ( return (
<InputNumber <InputNumber
disabled={!operator}
onChange={(value) => form.setFieldValue(fieldPath, value)}/> 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 ( return (
<Input <Input
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/> disabled={!operator}
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}
/>
); );
})() })()
} }
@@ -203,12 +266,12 @@ function FiltersSection({filters, form, bodyshop}) {
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
function SortersSection({sorters, form}) { function SortersSection({sorters}) {
const {t} = useTranslation(); const {t} = useTranslation();
return ( return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}> <Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
<Form.List name={["sorters"]}> <Form.List name={["sorters"]}>
{(fields, {add, remove, move}) => { {(fields, {add, remove}) => {
return ( return (
<div> <div>
Sorters Sorters

View File

@@ -8,6 +8,20 @@ import {uniqBy} from "lodash";
*/ */
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj); 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 * Valid internal reflections
* Note: This is intended for future functionality * Note: This is intended for future functionality
@@ -42,19 +56,44 @@ const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
* Generate special reflections * Generate special reflections
* @param bodyshop * @param bodyshop
* @param finalPath * @param finalPath
* @param t - i18n
* @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]} * @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
*/ */
const generateSpecialReflections = (bodyshop, finalPath) => { const generateSpecialReflections = (bodyshop, finalPath, t) => {
switch (finalPath) { 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': case 'cost_centers':
return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name'); return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
// Special case because Categories is an Array, not an Object. // Special case because Categories is an Array, not an Object.
case 'categories': case 'categories':
const catOptions = getValueFromPath(bodyshop, 'md_categories'); return generateOptionsFromArray(bodyshop, 'md_categories');
return uniqBy(catOptions.map((value) => ({
label: value,
value: value,
})), 'value');
case 'insurance_companies': case 'insurance_companies':
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name'); return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
case 'employee_teams': case 'employee_teams':
@@ -105,12 +144,13 @@ const generateBodyshopReflections = (bodyshop, finalPath) => {
* @param bodyshop * @param bodyshop
* @param upperPath * @param upperPath
* @param finalPath * @param finalPath
* @param t - i18n
* @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]} * @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
*/ */
const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => { const generateInternalReflections = ({bodyshop, upperPath, finalPath, t}) => {
switch (upperPath) { switch (upperPath) {
case 'special': case 'special':
return generateSpecialReflections(bodyshop, finalPath); return generateSpecialReflections(bodyshop, finalPath, t);
case 'bodyshop': case 'bodyshop':
return generateBodyshopReflections(bodyshop, finalPath); return generateBodyshopReflections(bodyshop, finalPath);
default: default:
@@ -118,4 +158,4 @@ const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
} }
}; };
export {generateInternalReflections,} export {generateInternalReflections}

View File

@@ -34,8 +34,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")), toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
setEmailOptions: (e) => dispatch(setEmailOptions(e)), setEmailOptions: (e) => dispatch(setEmailOptions(e)),
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function ScheduleJobModalContainer({ export function ScheduleJobModalContainer({
@@ -146,6 +146,7 @@ export function ScheduleJobModalContainer({
operation: AuditTrailMapping.appointmentinsert( operation: AuditTrailMapping.appointmentinsert(
DateTimeFormat(values.start) DateTimeFormat(values.start)
), ),
type: "appointmentinsert",
}); });
} }

View File

@@ -25,6 +25,8 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
return acc + value.bodyhrs; return acc + value.bodyhrs;
}, 0); }, 0);
const numJobs = entries.length;
return ( return (
<Card <Card
title={moment(date).format("D - ddd")} title={moment(date).format("D - ddd")}
@@ -33,17 +35,18 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
> >
<Statistic <Statistic
valueStyle={{ color: dailyBodyTarget > bodyHrs ? "red" : "green" }} valueStyle={{ color: dailyBodyTarget > bodyHrs ? "red" : "green" }}
label="B" label="Body"
value={bodyHrs.toFixed(1)} value={bodyHrs.toFixed(1)}
/> />
<Statistic <Statistic
valueStyle={{ color: dailyPaintTarget > paintHrs ? "red" : "green" }} valueStyle={{ color: dailyPaintTarget > paintHrs ? "red" : "green" }}
label="P" label="Refinish"
value={paintHrs.toFixed(1)} value={paintHrs.toFixed(1)}
/> />
<Divider style={{ margin: 0 }} /> <Divider style={{ margin: 0 }} />
<Statistic label="Total" value={(bodyHrs + paintHrs).toFixed(1)} />
<Statistic value={(bodyHrs + paintHrs).toFixed(1)} /> <Divider style={{ margin: 0 }} />
<Statistic label="Jobs" value={numJobs} />
</Card> </Card>
); );
} }

View File

@@ -29,10 +29,13 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
let ret = { let ret = {
todayBody: 0, todayBody: 0,
todayPaint: 0, todayPaint: 0,
todayJobs: 0,
weeklyPaint: 0, weeklyPaint: 0,
weeklyJobs: 0,
weeklyBody: 0, weeklyBody: 0,
toDateBody: 0, toDateBody: 0,
toDatePaint: 0, toDatePaint: 0,
toDateJobs: 0,
}; };
const today = moment(); const today = moment();
@@ -40,6 +43,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[today.format("YYYY-MM-DD")].forEach((d) => { dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
ret.todayBody = ret.todayBody + d.bodyhrs; ret.todayBody = ret.todayBody + d.bodyhrs;
ret.todayPaint = ret.todayPaint + d.painthrs; 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) => { dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
ret.weeklyBody = ret.weeklyBody + d.bodyhrs; ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
ret.weeklyPaint = ret.weeklyPaint + d.painthrs; ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
ret.weeklyJobs++;
}); });
} }
StartOfWeek = StartOfWeek.add(1, "day"); StartOfWeek = StartOfWeek.add(1, "day");
@@ -60,6 +65,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => { dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
ret.toDateBody = ret.toDateBody + d.bodyhrs; ret.toDateBody = ret.toDateBody + d.bodyhrs;
ret.toDatePaint = ret.toDatePaint + d.painthrs; ret.toDatePaint = ret.toDatePaint + d.painthrs;
ret.toDateJobs++;
}); });
} }
startOfMonth = startOfMonth.add(1, "day"); startOfMonth = startOfMonth.add(1, "day");
@@ -87,7 +93,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Statistic <Statistic
title={t("scoreboard.labels.dailytarget")} title={t("scoreboard.labels.dailytarget")}
value={bodyshop.scoreboard_target.dailyBodyTarget} value={bodyshop.scoreboard_target.dailyBodyTarget}
prefix="B" prefix={t("scoreboard.labels.bodyabbrev")}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
@@ -140,7 +146,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={bodyshop.scoreboard_target.dailyPaintTarget} value={bodyshop.scoreboard_target.dailyPaintTarget}
prefix="P" prefix={t("scoreboard.labels.refinishabbrev")}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
@@ -181,7 +187,12 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Divider style={{ margin: 5 }} /> <Divider style={{ margin: 5 }} />
</Row> </Row>
<Row> <Row>
<Col {...statSpans}></Col> <Col {...statSpans}>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.total")}
/>
</Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={(values.todayPaint + values.todayBody).toFixed(1)} value={(values.todayPaint + values.todayBody).toFixed(1)}
@@ -240,6 +251,29 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
/> />
</Col> </Col>
</Row> </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> </Col>
</Row> </Row>
</Card> </Card>

View File

@@ -5,6 +5,7 @@ import {
Input, Input,
InputNumber, InputNumber,
Select, Select,
Space,
Switch, Switch,
Typography, Typography,
} from "antd"; } from "antd";
@@ -17,6 +18,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
const SelectorDiv = styled.div` const SelectorDiv = styled.div`
.ant-form-item .ant-select { .ant-form-item .ant-select {
@@ -191,7 +193,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}> <LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}>
<Form.List name={["cdk_configuration", "payers"]}> <Form.List name={["cdk_configuration", "payers"]}>
{(fields, { add, remove }) => { {(fields, { add, remove, move }) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -249,11 +251,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
</Select> </Select>
</Form.Item> </Form.Item>
<DeleteFilled <Space align="center">
onClick={() => { <DeleteFilled
remove(field.name); onClick={() => {
}} remove(field.name);
/> }}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>
))} ))}
@@ -345,7 +354,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
id="costs" id="costs"
> >
<Form.List name={["md_responsibility_centers", "costs"]}> <Form.List name={["md_responsibility_centers", "costs"]}>
{(fields, { add, remove }) => { {(fields, { add, remove, move }) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -462,12 +471,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} /> <Input onBlur={handleBlur} />
</Form.Item> </Form.Item>
)} )}
<Space align="center">
<DeleteFilled <DeleteFilled
onClick={() => { onClick={() => {
remove(field.name); remove(field.name);
}} }}
/> />
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>
))} ))}
@@ -493,7 +508,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
id="profits" id="profits"
> >
<Form.List name={["md_responsibility_centers", "profits"]}> <Form.List name={["md_responsibility_centers", "profits"]}>
{(fields, { add, remove }) => { {(fields, { add, remove, move }) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -595,11 +610,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input onBlur={handleBlur} /> <Input onBlur={handleBlur} />
</Form.Item> </Form.Item>
)} )}
<DeleteFilled <Space align="center">
onClick={() => { <DeleteFilled
remove(field.name); onClick={() => {
}} remove(field.name);
/> }}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow> </LayoutFormRow>
</Form.Item> </Form.Item>
))} ))}

View File

@@ -7,7 +7,9 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort, statusSort } from "../../utils/sorters"; import { alphaSort, statusSort } from "../../utils/sorters";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component"; import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -45,6 +47,10 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.owner"), title: t("jobs.fields.owner"),
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link to={`/manage/owners/${record.owner.id}`}> <Link to={`/manage/owners/${record.owner.id}`}>
<OwnerNameDisplay ownerObject={record} /> <OwnerNameDisplay ownerObject={record} />
@@ -63,9 +69,15 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
title: t("jobs.fields.status"), title: t("jobs.fields.status"),
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses), sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.statuses),
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters: bodyshop.md_ro_statuses.statuses.map((status) => ({
text: status,
value: status,
})),
onFilter: (value, record) => value.includes(record.status),
}, },
{ {

View File

@@ -4,8 +4,9 @@ import queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { pageLimit } from "../../utils/config";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import {pageLimit} from "../../utils/config"; import { alphaSort } from './../../utils/sorters';
export default function VehiclesListComponent({ export default function VehiclesListComponent({
loading, loading,
vehicles, vehicles,
@@ -31,6 +32,8 @@ export default function VehiclesListComponent({
title: t("vehicles.fields.v_vin"), title: t("vehicles.fields.v_vin"),
dataIndex: "v_vin", dataIndex: "v_vin",
key: "v_vin", key: "v_vin",
sorter: (a, b) => alphaSort(a.v_vin, b.v_vin),
sortOrder: state.sortedInfo.columnKey === "v_vin" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link to={"/manage/vehicles/" + record.id}> <Link to={"/manage/vehicles/" + record.id}>
<VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay> <VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay>
@@ -51,8 +54,10 @@ export default function VehiclesListComponent({
}, },
{ {
title: t("vehicles.fields.plate_no"), title: t("vehicles.fields.plate_no"),
dataIndex: "plate", dataIndex: "plate_no",
key: "plate", key: "plate_no",
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder: state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return ( return (
<span>{`${record.plate_st || ""} | ${record.plate_no || ""}`}</span> <span>{`${record.plate_st || ""} | ${record.plate_no || ""}`}</span>

View File

@@ -4,6 +4,8 @@ import { getAuth, updatePassword, updateProfile } from "firebase/auth";
import { getFirestore } from "firebase/firestore"; import { getFirestore } from "firebase/firestore";
import { getMessaging, getToken, onMessage } from "firebase/messaging"; import { getMessaging, getToken, onMessage } from "firebase/messaging";
import { store } from "../redux/store"; import { store } from "../redux/store";
import axios from "axios";
import { checkBeta } from "../utils/handleBeta";
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG); const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
initializeApp(config); initializeApp(config);
@@ -86,6 +88,18 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
null, null,
...additionalParams, ...additionalParams,
}; };
axios.post("/ioevent", {
useremail:
(state.user && state.user.currentUser && state.user.currentUser.email) ||
null,
bodyshopid:
(state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
operationName: eventName,
variables: additionalParams,
dbevent: false,
env: checkBeta() ? "beta" : "master",
});
// console.log( // console.log(
// "%c[Analytics]", // "%c[Analytics]",
// "background-color: green ;font-weight:bold;", // "background-color: green ;font-weight:bold;",

View File

@@ -40,6 +40,7 @@ export const INSERT_AUDIT_TRAIL = gql`
bodyshopid bodyshopid
created created
operation operation
type
useremail useremail
} }
} }

View File

@@ -1862,6 +1862,7 @@ export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
ownr_co_nm ownr_co_nm
ownr_ph1 ownr_ph1
ownr_ph2 ownr_ph2
ownerid
plate_no plate_no
plate_st plate_st
v_vin v_vin

View File

@@ -31,6 +31,7 @@ export const QUERY_VEHICLE_BY_ID = gql`
jobs(order_by: { date_open: desc }) { jobs(order_by: { date_open: desc }) {
id id
ro_number ro_number
ownr_co_nm
ownr_fn ownr_fn
ownr_ln ownr_ln
owner { owner {

View File

@@ -40,8 +40,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
@@ -127,6 +127,7 @@ export function DmsContainer({
insertAuditTrail({ insertAuditTrail({
jobid: payload, jobid: payload,
operation: AuditTrailMapping.jobexported(), operation: AuditTrailMapping.jobexported(),
type: "jobexported",
}); });
history.push("/manage/accounting/receivables"); history.push("/manage/accounting/receivables");
}); });

View File

@@ -12,7 +12,8 @@ import AlertComponent from "../../components/alert/alert.component";
import { QUERY_EXPORT_LOG_PAGINATED } from "../../graphql/accounting.queries"; import { QUERY_EXPORT_LOG_PAGINATED } from "../../graphql/accounting.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import {pageLimit} from "../../utils/config"; import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "./../../utils/sorters";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -34,11 +35,43 @@ export function ExportLogsPageComponent({ bodyshop }) {
limit: pageLimit, limit: pageLimit,
order: [ order: [
{ {
[sortcolumn || "created_at"]: sortorder ...(sortcolumn === "ro_number"
? sortorder === "descend" ? {
? "desc" job: {
: "asc" [sortcolumn || "created_at"]: sortorder
: "desc", ? 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"), title: t("general.labels.created_at"),
dataIndex: "created_at", dataIndex: "created_at",
key: "created_at", key: "created_at",
sorter: (a, b) => dateSort(a.created_at, b.created_at),
sortOrder: sortcolumn === "created_at" && sortorder,
render: (text, record) => ( render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter> <DateTimeFormatter>{record.created_at}</DateTimeFormatter>
), ),
@@ -81,7 +116,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => render: (text, record) =>
record.job && ( record.job && (
<Link to={`/manage/jobs/${record.job.id}`}> <Link to={`/manage/jobs/${record.job.id}`}>
@@ -93,6 +129,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
title: t("bills.fields.invoice_number"), title: t("bills.fields.invoice_number"),
dataIndex: "invoice_number", dataIndex: "invoice_number",
key: "invoice_number", key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder: sortcolumn === "invoice_number" && sortorder,
render: (text, record) => render: (text, record) =>
record.bill && ( record.bill && (
<Link to={"/manage/bills?billid=" + (record.bill && record.bill.id)}> <Link to={"/manage/bills?billid=" + (record.bill && record.bill.id)}>
@@ -104,6 +142,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
title: t("payments.fields.paymentnum"), title: t("payments.fields.paymentnum"),
dataIndex: "paymentnum", dataIndex: "paymentnum",
key: "paymentnum", key: "paymentnum",
sorter: (a, b) => alphaSort(a.paymentnum, b.paymentnum),
sortOrder: sortcolumn === "paymentnum" && sortorder,
render: (text, record) => render: (text, record) =>
record.payment && ( record.payment && (
<Link <Link
@@ -120,6 +160,13 @@ export function ExportLogsPageComponent({ bodyshop }) {
title: t("general.labels.successful"), title: t("general.labels.successful"),
dataIndex: "successful", dataIndex: "successful",
key: "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) => ( render: (text, record) => (
<Checkbox disabled checked={record.successful} /> <Checkbox disabled checked={record.successful} />
), ),

View File

@@ -47,11 +47,11 @@ const mapStateToProps = createStructuredSelector({
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, }) { export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const client = useApolloClient(); const client = useApolloClient();
@@ -121,6 +121,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, })
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
operation: AuditTrailMapping.jobinvoiced(), operation: AuditTrailMapping.jobinvoiced(),
type: "jobinvoiced",
}); });
// history.push(`/manage/jobs/${job.id}`); // history.push(`/manage/jobs/${job.id}`);
} else { } else {

View File

@@ -29,6 +29,7 @@ import { createStructuredSelector } from "reselect";
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component"; 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 JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container"; 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 JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container"; import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component"; 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 AuditTrailMapping from "../../utils/AuditTrailMappings";
import UndefinedToNull from "../../utils/undefinedtonull"; import UndefinedToNull from "../../utils/undefinedtonull";
import { DateTimeFormat } from "./../../utils/DateFormatter"; import { DateTimeFormat } from "./../../utils/DateFormatter";
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -63,8 +63,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) => setPrintCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "printCenter" })), dispatch(setModalContext({ context: context, modal: "printCenter" })),
insertAuditTrail: ({ jobid, operation }) => insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation })), dispatch(insertAuditTrail({ jobid, operation, type })),
}); });
export function JobsDetailPage({ export function JobsDetailPage({
bodyshop, bodyshop,
@@ -177,6 +177,7 @@ export function JobsDetailPage({
? DateTimeFormat(changedAuditFields[key]) ? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key] : changedAuditFields[key]
), ),
type: "jobfieldchange",
}); });
}); });
@@ -334,15 +335,23 @@ export function JobsDetailPage({
> >
<JobsDetailLaborContainer job={job} jobId={job.id} /> <JobsDetailLaborContainer job={job} jobId={job.id} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
forceRender forceRender
tab={<span><BarsOutlined />{t('menus.jobsdetail.lifecycle')}</span>} tab={
key="lifecycle" <span>
> <BarsOutlined />
<JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses}/> {t("menus.jobsdetail.lifecycle")}
</Tabs.TabPane> </span>
}
key="lifecycle"
>
<JobLifecycleComponent
job={job}
statuses={bodyshop.md_ro_statuses}
/>
</Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
forceRender forceRender
tab={ tab={
<span> <span>

View File

@@ -54,9 +54,9 @@ export const setOnline = (isOnline) => ({
payload: isOnline, payload: isOnline,
}); });
export const insertAuditTrail = ({ jobid, billid, operation }) => ({ export const insertAuditTrail = ({ jobid, billid, operation, type }) => ({
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL, type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
payload: { jobid, billid, operation }, payload: { jobid, billid, operation, type },
}); });
export const setProblemJobs = (problemJobs) => ({ export const setProblemJobs = (problemJobs) => ({
type: ApplicationActionTypes.SET_PROBLEM_JOBS, type: ApplicationActionTypes.SET_PROBLEM_JOBS,

View File

@@ -266,7 +266,7 @@ export function* onInsertAuditTrail() {
} }
export function* insertAuditTrailSaga({ export function* insertAuditTrailSaga({
payload: { jobid, billid, operation }, payload: { jobid, billid, operation, type },
}) { }) {
const state = yield select(); const state = yield select();
const bodyshop = state.user.bodyshop; const bodyshop = state.user.bodyshop;
@@ -278,6 +278,7 @@ export function* insertAuditTrailSaga({
jobid, jobid,
billid, billid,
operation, operation,
type,
useremail: currentUser.email, useremail: currentUser.email,
}, },
}; };

View File

@@ -259,6 +259,7 @@
"saving": "Error encountered while saving. {{message}}" "saving": "Error encountered while saving. {{message}}"
}, },
"fields": { "fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"address1": "Address 1", "address1": "Address 1",
"address2": "Address 2", "address2": "Address 2",
"appt_alt_transport": "Appointment Alternative Transportation Options", "appt_alt_transport": "Appointment Alternative Transportation Options",
@@ -477,7 +478,6 @@
"editaccess": "Users -> Edit access" "editaccess": "Users -> Edit access"
} }
}, },
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"responsibilitycenter": "Responsibility Center", "responsibilitycenter": "Responsibility Center",
"responsibilitycenter_accountdesc": "Account Description", "responsibilitycenter_accountdesc": "Account Description",
"responsibilitycenter_accountitem": "Item", "responsibilitycenter_accountitem": "Item",
@@ -608,7 +608,7 @@
"dms": { "dms": {
"cdk": { "cdk": {
"controllist": "Control Number List", "controllist": "Control Number List",
"payers": "CDK Payers" "payers": "Payers"
}, },
"cdk_dealerid": "CDK Dealer ID", "cdk_dealerid": "CDK Dealer ID",
"pbs_serialnumber": "PBS Serial Number", "pbs_serialnumber": "PBS Serial Number",
@@ -844,8 +844,8 @@
"notconfigured": "You do not have any current CSI Question Sets configured.", "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.", "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}}.",
"surveycompletesubtitle": "This survey was already completed on {{date}}." "surveycompletetitle": "Survey previously completed"
}, },
"fields": { "fields": {
"completedon": "Completed On", "completedon": "Completed On",
@@ -854,13 +854,13 @@
"validuntil": "Valid Until" "validuntil": "Valid Until"
}, },
"labels": { "labels": {
"copyright": "Copyright © $t(titles.app). All Rights Reserved.",
"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.",
"nologgedinuser": "Please log out of $t(titles.app)", "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.", "nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.",
"noneselected": "No response selected.", "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": { "successes": {
"created": "CSI created successfully.", "created": "CSI created successfully.",
@@ -898,7 +898,8 @@
"scheduledindate": "Sheduled In Today: {{date}}", "scheduledindate": "Sheduled In Today: {{date}}",
"scheduledintoday": "Sheduled In Today", "scheduledintoday": "Sheduled In Today",
"scheduledoutdate": "Sheduled Out Today: {{date}}", "scheduledoutdate": "Sheduled Out Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today" "scheduledouttoday": "Sheduled Out Today",
"joblifecycle": "Job Lifecycle"
} }
}, },
"dms": { "dms": {
@@ -1236,7 +1237,15 @@
"relative_end": "Relative End", "relative_end": "Relative End",
"relative_start": "Relative Start", "relative_start": "Relative Start",
"start": "Start", "start": "Start",
"value": "Value" "value": "Value",
"status": "Status",
"percentage": "Percentage",
"human_readable": "Human Readable",
"status_count": "In Status"
},
"titles": {
"dashboard": "Job Lifecycle",
"top_durations": "Top Durations"
}, },
"content": { "content": {
"current_status_accumulated_time": "Current Status Accumulated Time", "current_status_accumulated_time": "Current Status Accumulated Time",
@@ -1248,7 +1257,9 @@
"title": "Job Lifecycle Component", "title": "Job Lifecycle Component",
"title_durations": "Historical Status Durations", "title_durations": "Historical Status Durations",
"title_loading": "Loading", "title_loading": "Loading",
"title_transitions": "Transitions" "title_transitions": "Transitions",
"calculated_based_on": "Calculated based on",
"jobs_in_since": "Jobs in since"
}, },
"errors": { "errors": {
"fetch": "Error getting Job Lifecycle Data" "fetch": "Error getting Job Lifecycle Data"
@@ -1823,6 +1834,7 @@
"job": "Job Details", "job": "Job Details",
"jobcosting": "Job Costing", "jobcosting": "Job Costing",
"jobtotals": "Job Totals", "jobtotals": "Job Totals",
"labor_hrs": "B/P/T Hrs",
"labor_rates_subtotal": "Labor Rates Subtotal", "labor_rates_subtotal": "Labor Rates Subtotal",
"laborallocations": "Labor Allocations", "laborallocations": "Labor Allocations",
"labortotals": "Labor Totals", "labortotals": "Labor Totals",
@@ -2417,6 +2429,7 @@
"invoice_total_payable": "Invoice (Total Payable)", "invoice_total_payable": "Invoice (Total Payable)",
"iou_form": "IOU Form", "iou_form": "IOU Form",
"job_costing_ro": "Job Costing", "job_costing_ro": "Job Costing",
"job_lifecycle_ro": "Job Lifecycle",
"job_notes": "Job Notes", "job_notes": "Job Notes",
"key_tag": "Key Tag", "key_tag": "Key Tag",
"labels": { "labels": {
@@ -2583,15 +2596,17 @@
}, },
"labels": { "labels": {
"advanced_filters": "Advanced Filters and Sorters", "advanced_filters": "Advanced Filters and Sorters",
"advanced_filters_show": "Show", "advanced_filters_false": "False",
"advanced_filters_hide": "Hide",
"advanced_filters_filters": "Filters",
"advanced_filters_sorters": "Sorters",
"advanced_filters_filter_field": "Field", "advanced_filters_filter_field": "Field",
"advanced_filters_sorter_field": "Field",
"advanced_filters_sorter_direction": "Direction",
"advanced_filters_filter_operator": "Operator", "advanced_filters_filter_operator": "Operator",
"advanced_filters_filter_value": "Value", "advanced_filters_filter_value": "Value",
"advanced_filters_filters": "Filters",
"advanced_filters_hide": "Hide",
"advanced_filters_show": "Show",
"advanced_filters_sorter_direction": "Direction",
"advanced_filters_sorter_field": "Field",
"advanced_filters_sorters": "Sorters",
"advanced_filters_true": "True",
"dates": "Dates", "dates": "Dates",
"employee": "Employee", "employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}", "filterson": "Filters on {{object}}: {{field}}",
@@ -2673,6 +2688,8 @@
"job_costing_ro_date_summary": "Job Costing by RO - Summary", "job_costing_ro_date_summary": "Job Costing by RO - Summary",
"job_costing_ro_estimator": "Job Costing by Estimator", "job_costing_ro_estimator": "Job Costing by Estimator",
"job_costing_ro_ins_co": "Job Costing by RO Source", "job_costing_ro_ins_co": "Job Costing by RO Source",
"job_lifecycle_date_detail": "Job Lifecycle by Date - Detail",
"job_lifecycle_date_summary": "Job Lifecycle by Date - Summary",
"jobs_completed_not_invoiced": "Jobs Completed not Invoiced", "jobs_completed_not_invoiced": "Jobs Completed not Invoiced",
"jobs_invoiced_not_exported": "Jobs Invoiced not Exported", "jobs_invoiced_not_exported": "Jobs Invoiced not Exported",
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation", "jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
@@ -2762,6 +2779,7 @@
"allemployeetimetickets": "All Employee Time Tickets", "allemployeetimetickets": "All Employee Time Tickets",
"asoftodaytarget": "As of Today", "asoftodaytarget": "As of Today",
"body": "Body", "body": "Body",
"bodyabbrev": "B",
"bodycharttitle": "Body Targets vs Actual", "bodycharttitle": "Body Targets vs Actual",
"calendarperiod": "Periods based on calendar weeks/months.", "calendarperiod": "Periods based on calendar weeks/months.",
"combinedcharttitle": "Combined Targets vs Actual", "combinedcharttitle": "Combined Targets vs Actual",
@@ -2778,6 +2796,7 @@
"productivestatistics": "Productive Hours Statistics", "productivestatistics": "Productive Hours Statistics",
"productivetimeticketsoverdate": "Productive Hours over Selected Dates", "productivetimeticketsoverdate": "Productive Hours over Selected Dates",
"refinish": "Refinish", "refinish": "Refinish",
"refinishabbrev": "R",
"refinishcharttitle": "Refinish Targets vs Actual", "refinishcharttitle": "Refinish Targets vs Actual",
"targets": "Targets", "targets": "Targets",
"thismonth": "This Month", "thismonth": "This Month",
@@ -2785,6 +2804,7 @@
"timetickets": "Time Tickets", "timetickets": "Time Tickets",
"timeticketsemployee": "Time Tickets by Employee", "timeticketsemployee": "Time Tickets by Employee",
"todateactual": "Actual (MTD)", "todateactual": "Actual (MTD)",
"total": "Total",
"totalhrs": "Total Hours", "totalhrs": "Total Hours",
"totaloverperiod": "Total over Selected Dates", "totaloverperiod": "Total over Selected Dates",
"weeklyactual": "Actual (W)", "weeklyactual": "Actual (W)",

View File

@@ -259,6 +259,7 @@
"saving": "" "saving": ""
}, },
"fields": { "fields": {
"ReceivableCustomField": "",
"address1": "", "address1": "",
"address2": "", "address2": "",
"appt_alt_transport": "", "appt_alt_transport": "",
@@ -477,7 +478,6 @@
"editaccess": "" "editaccess": ""
} }
}, },
"ReceivableCustomField": "",
"responsibilitycenter": "", "responsibilitycenter": "",
"responsibilitycenter_accountdesc": "", "responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "", "responsibilitycenter_accountitem": "",
@@ -844,8 +844,8 @@
"notconfigured": "", "notconfigured": "",
"notfoundsubtitle": "", "notfoundsubtitle": "",
"notfoundtitle": "", "notfoundtitle": "",
"surveycompletetitle": "", "surveycompletesubtitle": "",
"surveycompletesubtitle": "" "surveycompletetitle": ""
}, },
"fields": { "fields": {
"completedon": "", "completedon": "",
@@ -854,13 +854,13 @@
"validuntil": "" "validuntil": ""
}, },
"labels": { "labels": {
"copyright": "",
"greeting": "",
"intro": "",
"nologgedinuser": "", "nologgedinuser": "",
"nologgedinuser_sub": "", "nologgedinuser_sub": "",
"noneselected": "", "noneselected": "",
"title": "", "title": ""
"greeting": "",
"intro": "",
"copyright": ""
}, },
"successes": { "successes": {
"created": "", "created": "",
@@ -898,7 +898,8 @@
"scheduledindate": "", "scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "", "scheduledoutdate": "",
"scheduledouttoday": "" "scheduledouttoday": "",
"joblifecycle": ""
} }
}, },
"dms": { "dms": {
@@ -1236,7 +1237,15 @@
"relative_end": "", "relative_end": "",
"relative_start": "", "relative_start": "",
"start": "", "start": "",
"value": "" "value": "",
"status": "",
"percentage": "",
"human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
}, },
"content": { "content": {
"current_status_accumulated_time": "", "current_status_accumulated_time": "",
@@ -1248,7 +1257,9 @@
"title": "", "title": "",
"title_durations": "", "title_durations": "",
"title_loading": "", "title_loading": "",
"title_transitions": "" "title_transitions": "",
"calculated_based_on": "",
"jobs_in_since": ""
}, },
"errors": { "errors": {
"fetch": "Error al obtener los datos del ciclo de vida del trabajo" "fetch": "Error al obtener los datos del ciclo de vida del trabajo"
@@ -1823,6 +1834,7 @@
"job": "", "job": "",
"jobcosting": "", "jobcosting": "",
"jobtotals": "", "jobtotals": "",
"labor_hrs": "",
"labor_rates_subtotal": "", "labor_rates_subtotal": "",
"laborallocations": "", "laborallocations": "",
"labortotals": "", "labortotals": "",
@@ -2417,6 +2429,7 @@
"invoice_total_payable": "", "invoice_total_payable": "",
"iou_form": "", "iou_form": "",
"job_costing_ro": "", "job_costing_ro": "",
"job_lifecycle_ro": "",
"job_notes": "", "job_notes": "",
"key_tag": "", "key_tag": "",
"labels": { "labels": {
@@ -2583,15 +2596,17 @@
}, },
"labels": { "labels": {
"advanced_filters": "", "advanced_filters": "",
"advanced_filters_show": "", "advanced_filters_false": "",
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
"advanced_filters_filter_field": "", "advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_filter_operator": "", "advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "", "advanced_filters_filter_value": "",
"advanced_filters_filters": "",
"advanced_filters_hide": "",
"advanced_filters_show": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorters": "",
"advanced_filters_true": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",
@@ -2673,6 +2688,8 @@
"job_costing_ro_date_summary": "", "job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "", "job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "", "job_costing_ro_ins_co": "",
"job_lifecycle_date_detail": "",
"job_lifecycle_date_summary": "",
"jobs_completed_not_invoiced": "", "jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "", "jobs_invoiced_not_exported": "",
"jobs_reconcile": "", "jobs_reconcile": "",
@@ -2762,6 +2779,7 @@
"allemployeetimetickets": "", "allemployeetimetickets": "",
"asoftodaytarget": "", "asoftodaytarget": "",
"body": "", "body": "",
"bodyabbrev": "",
"bodycharttitle": "", "bodycharttitle": "",
"calendarperiod": "", "calendarperiod": "",
"combinedcharttitle": "", "combinedcharttitle": "",
@@ -2778,6 +2796,7 @@
"productivestatistics": "", "productivestatistics": "",
"productivetimeticketsoverdate": "", "productivetimeticketsoverdate": "",
"refinish": "", "refinish": "",
"refinishabbrev": "",
"refinishcharttitle": "", "refinishcharttitle": "",
"targets": "", "targets": "",
"thismonth": "", "thismonth": "",
@@ -2785,6 +2804,7 @@
"timetickets": "", "timetickets": "",
"timeticketsemployee": "", "timeticketsemployee": "",
"todateactual": "", "todateactual": "",
"total": "",
"totalhrs": "", "totalhrs": "",
"totaloverperiod": "", "totaloverperiod": "",
"weeklyactual": "", "weeklyactual": "",

View File

@@ -259,6 +259,7 @@
"saving": "" "saving": ""
}, },
"fields": { "fields": {
"ReceivableCustomField": "",
"address1": "", "address1": "",
"address2": "", "address2": "",
"appt_alt_transport": "", "appt_alt_transport": "",
@@ -477,7 +478,6 @@
"editaccess": "" "editaccess": ""
} }
}, },
"ReceivableCustomField": "",
"responsibilitycenter": "", "responsibilitycenter": "",
"responsibilitycenter_accountdesc": "", "responsibilitycenter_accountdesc": "",
"responsibilitycenter_accountitem": "", "responsibilitycenter_accountitem": "",
@@ -844,8 +844,8 @@
"notconfigured": "", "notconfigured": "",
"notfoundsubtitle": "", "notfoundsubtitle": "",
"notfoundtitle": "", "notfoundtitle": "",
"surveycompletetitle": "", "surveycompletesubtitle": "",
"surveycompletesubtitle": "" "surveycompletetitle": ""
}, },
"fields": { "fields": {
"completedon": "", "completedon": "",
@@ -854,13 +854,13 @@
"validuntil": "" "validuntil": ""
}, },
"labels": { "labels": {
"copyright": "",
"greeting": "",
"intro": "",
"nologgedinuser": "", "nologgedinuser": "",
"nologgedinuser_sub": "", "nologgedinuser_sub": "",
"noneselected": "", "noneselected": "",
"title": "", "title": ""
"greeting": "",
"intro": "",
"copyright": ""
}, },
"successes": { "successes": {
"created": "", "created": "",
@@ -1236,7 +1236,15 @@
"relative_end": "", "relative_end": "",
"relative_start": "", "relative_start": "",
"start": "", "start": "",
"value": "" "value": "",
"status": "",
"percentage": "",
"human_readable": "",
"status_count": ""
},
"titles": {
"dashboard": "",
"top_durations": ""
}, },
"content": { "content": {
"current_status_accumulated_time": "", "current_status_accumulated_time": "",
@@ -1248,7 +1256,10 @@
"title": "", "title": "",
"title_durations": "", "title_durations": "",
"title_loading": "", "title_loading": "",
"title_transitions": "" "title_transitions": "",
"calculated_based_on": "",
"jobs_in_since": "",
"joblifecycle": ""
}, },
"errors": { "errors": {
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches" "fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
@@ -1823,6 +1834,7 @@
"job": "", "job": "",
"jobcosting": "", "jobcosting": "",
"jobtotals": "", "jobtotals": "",
"labor_hrs": "",
"labor_rates_subtotal": "", "labor_rates_subtotal": "",
"laborallocations": "", "laborallocations": "",
"labortotals": "", "labortotals": "",
@@ -2417,6 +2429,7 @@
"invoice_total_payable": "", "invoice_total_payable": "",
"iou_form": "", "iou_form": "",
"job_costing_ro": "", "job_costing_ro": "",
"job_lifecycle_ro": "",
"job_notes": "", "job_notes": "",
"key_tag": "", "key_tag": "",
"labels": { "labels": {
@@ -2583,15 +2596,17 @@
}, },
"labels": { "labels": {
"advanced_filters": "", "advanced_filters": "",
"advanced_filters_show": "", "advanced_filters_false": "",
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
"advanced_filters_filter_field": "", "advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_filter_operator": "", "advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "", "advanced_filters_filter_value": "",
"advanced_filters_filters": "",
"advanced_filters_hide": "",
"advanced_filters_show": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorters": "",
"advanced_filters_true": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",
@@ -2673,6 +2688,8 @@
"job_costing_ro_date_summary": "", "job_costing_ro_date_summary": "",
"job_costing_ro_estimator": "", "job_costing_ro_estimator": "",
"job_costing_ro_ins_co": "", "job_costing_ro_ins_co": "",
"job_lifecycle_date_detail": "",
"job_lifecycle_date_summary": "",
"jobs_completed_not_invoiced": "", "jobs_completed_not_invoiced": "",
"jobs_invoiced_not_exported": "", "jobs_invoiced_not_exported": "",
"jobs_reconcile": "", "jobs_reconcile": "",
@@ -2762,6 +2779,7 @@
"allemployeetimetickets": "", "allemployeetimetickets": "",
"asoftodaytarget": "", "asoftodaytarget": "",
"body": "", "body": "",
"bodyabbrev": "",
"bodycharttitle": "", "bodycharttitle": "",
"calendarperiod": "", "calendarperiod": "",
"combinedcharttitle": "", "combinedcharttitle": "",
@@ -2778,6 +2796,7 @@
"productivestatistics": "", "productivestatistics": "",
"productivetimeticketsoverdate": "", "productivetimeticketsoverdate": "",
"refinish": "", "refinish": "",
"refinishabbrev": "",
"refinishcharttitle": "", "refinishcharttitle": "",
"targets": "", "targets": "",
"thismonth": "", "thismonth": "",
@@ -2785,6 +2804,7 @@
"timetickets": "", "timetickets": "",
"timeticketsemployee": "", "timeticketsemployee": "",
"todateactual": "", "todateactual": "",
"total": "",
"totalhrs": "", "totalhrs": "",
"totaloverperiod": "", "totaloverperiod": "",
"weeklyactual": "", "weeklyactual": "",

View File

@@ -514,6 +514,14 @@ export const TemplateList = (type, context) => {
group: "financial", group: "financial",
dms: true, dms: true,
}, },
job_lifecycle_ro: {
title: i18n.t("printcenter.jobs.job_lifecycle_ro"),
description: "",
subject: i18n.t("printcenter.jobs.job_lifecycle_ro"),
key: "job_lifecycle_ro",
disabled: false,
group: "post",
},
} }
: {}), : {}),
...(!type || type === "job_special" ...(!type || type === "job_special"
@@ -2048,6 +2056,30 @@ export const TemplateList = (type, context) => {
datedisable: true, datedisable: true,
group: "customers", group: "customers",
}, },
job_lifecycle_date_detail: {
title: i18n.t("reportcenter.templates.job_lifecycle_date_detail"),
subject: i18n.t("reportcenter.templates.job_lifecycle_date_detail"),
key: "job_lifecycle_date_detail",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "jobs",
},
job_lifecycle_date_summary: {
title: i18n.t("reportcenter.templates.job_lifecycle_date_summary"),
subject: i18n.t("reportcenter.templates.job_lifecycle_date_summary"),
key: "job_lifecycle_date_summary",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "jobs",
},
} }
: {}), : {}),
...(!type || type === "courtesycarcontract" ...(!type || type === "courtesycarcontract"

View File

@@ -2,22 +2,66 @@ import {Kind, parse, print, visit} from "graphql";
import client from "./GraphQLClient"; import client from "./GraphQLClient";
import {gql} from "@apollo/client"; 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 = [ const STRING_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_like", label: "contains"}, {value: "_like", label: "contains"},
{value: "_nlike", label: "does not contain"}, {value: "_nlike", label: "does not contain"},
{value: "_ilike", label: "contains case-insensitive"}, {value: "_ilike", label: "contains case-insensitive"},
{value: "_nilike", label: "does not contain 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 = [ const NUMBER_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"}, {value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"}, {value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"}, {value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less 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 = [ const ORDER_BY_OPERATORS = [
{value: "asc", label: "ascending"}, {value: "asc", label: "ascending"},
{value: "desc", label: "descending"} {value: "desc", label: "descending"}
@@ -31,7 +75,6 @@ export function getOrderOperatorsByType() {
return ORDER_BY_OPERATORS; return ORDER_BY_OPERATORS;
} }
/** /**
* Get the available operators for filtering * Get the available operators for filtering
* @param type * @param type
@@ -40,13 +83,14 @@ export function getOrderOperatorsByType() {
export function getWhereOperatorsByType(type = 'string') { export function getWhereOperatorsByType(type = 'string') {
const operators = { const operators = {
string: STRING_OPERATORS, string: STRING_OPERATORS,
number: NUMBER_OPERATORS number: NUMBER_OPERATORS,
boolean: BOOLEAN_OPERATORS,
bool: BOOLEAN_OPERATORS,
date: DATE_OPERATORS
}; };
return operators[type]; return operators[type];
} }
/* eslint-disable no-loop-func */
/** /**
* Parse a GraphQL query into an AST * Parse a GraphQL query into an AST
* @param query * @param query
@@ -78,11 +122,9 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
// Parse the query and apply the filters and sorters // Parse the query and apply the filters and sorters
const ast = parseQuery(templateQueryToExecute); const ast = parseQuery(templateQueryToExecute);
let filterFields = [];
if (templateObject?.filters && templateObject?.filters?.length) { if (templateObject?.filters && templateObject?.filters?.length) {
applyFilters(ast, templateObject.filters, filterFields); applyFilters(ast, templateObject.filters);
wrapFiltersInAnd(ast, filterFields);
} }
if (templateObject?.sorters && templateObject?.sorters?.length) { if (templateObject?.sorters && templateObject?.sorters?.length) {
@@ -109,7 +151,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
return {contextData, useShopSpecificTemplate}; return {contextData, useShopSpecificTemplate};
} }
/** /**
* Apply sorters to the AST * Apply sorters to the AST
* @param ast * @param ast
@@ -149,16 +190,16 @@ export function applySorters(ast, sorters) {
if (!orderByArg) { if (!orderByArg) {
orderByArg = { orderByArg = {
kind: Kind.ARGUMENT, kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'order_by' }, name: {kind: Kind.NAME, value: 'order_by'},
value: { kind: Kind.OBJECT, fields: [] }, value: {kind: Kind.OBJECT, fields: []},
}; };
currentSelection.arguments.push(orderByArg); currentSelection.arguments.push(orderByArg);
} }
const sorterField = { const sorterField = {
kind: Kind.OBJECT_FIELD, kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetFieldName }, name: {kind: Kind.NAME, value: targetFieldName},
value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions value: {kind: Kind.ENUM, value: sorter.direction}, // Adjust if your schema uses a different type for sorting directions
}; };
// Add the new sorter condition // Add the new sorter condition
@@ -170,10 +211,59 @@ export function applySorters(ast, sorters) {
}); });
} }
/**
* 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 * Apply filters to the AST
* @param ast * @param ast
* @param filters * @param filters
* @returns {ASTNode}
*/ */
export function applyFilters(ast, filters) { export function applyFilters(ast, filters) {
return visit(ast, { return visit(ast, {
@@ -182,192 +272,197 @@ export function applyFilters(ast, filters) {
filters.forEach(filter => { filters.forEach(filter => {
const fieldPath = filter.field.split('.'); const fieldPath = filter.field.split('.');
let topLevel = false; let topLevel = false;
let topLevelSub = false;
// Determine if the filter should be applied at the top level // Determine if the filter should be applied at the top level
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { if (fieldPath.length === 2) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevel = true; topLevel = true;
} }
if (topLevel) { if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
// Construct the filter for a top-level application fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
const targetFieldName = fieldPath[fieldPath.length - 1]; topLevelSub = true;
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
const nestedFilter = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetFieldName },
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: filter.operator },
value: filterValue,
}],
},
};
// Find or create the where argument for the top-level field
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'where' },
value: { kind: Kind.OBJECT, fields: [] },
};
const topLevelSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: path },
value: { kind: Kind.OBJECT, fields: [] }
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(nestedFilter);
} else { // Directly under the top level
whereArg.value.fields.push(nestedFilter);
}
} else {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
let whereArgFound = false;
// Iterate over the fieldPath, except for the last entry, to navigate the structure
for (let i = 0; i < fieldPath.length - 1; i++) {
const fieldName = fieldPath[i];
let fieldFound = false;
// Check if the current selection has a selectionSet and selections
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
// Look for the field in the current selection's selections
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
if (selection) {
// Move down the AST to the found selection
currentSelection = selection;
fieldFound = true;
}
}
// If the field was not found in the current path, it's an issue
if (!fieldFound) {
console.error(`Field ${fieldName} not found in the current selection.`);
return; // Exit the loop and function due to error
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied
// Check if the 'where' argument already exists in the current selection
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
if (whereArg) {
whereArgFound = true;
} else {
// If not found, create a new 'where' argument for the current selection
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'where' },
value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter
});
}
// Assuming the last entry in fieldPath is the field to apply the filter on
const targetField = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
// Construct the filter field object
const filterField = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetField },
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: filter.operator },
value: filterValue,
}],
},
};
// Add the filter field to the 'where' clause of the current selection
if (whereArgFound) {
whereArg.value.fields.push(filterField);
} else {
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
}
} }
// 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 * Get the GraphQL kind for a value
* @param value * @param value
* @returns {Kind|Kind.INT} * @returns {Kind|Kind.INT}
*/ */
function getGraphQLKind(value) { function getGraphQLKind(value) {
if (typeof value === 'number') { if (Array.isArray(value)) {
return Kind.LIST;
} else if (typeof value === 'number') {
return value % 1 === 0 ? Kind.INT : Kind.FLOAT; return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
} else if (typeof value === 'boolean') { } else if (typeof value === 'boolean') {
return Kind.BOOLEAN; return Kind.BOOLEAN;
} else if (typeof value === 'string') { } else if (typeof value === 'string') {
return Kind.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
} }
// Extend with more types as needed
}
/**
* Wrap filters in an 'and' object
* @param ast
* @param filterFields
*/
export function wrapFiltersInAnd(ast, filterFields) {
visit(ast, {
OperationDefinition: {
enter(node) {
node.selectionSet.selections.forEach((selection) => {
let whereArg = selection.arguments.find(arg => arg.name.value === 'where');
if (filterFields.length > 1) {
const andFilter = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: '_and'},
value: {kind: Kind.LIST, values: filterFields}
};
whereArg.value.fields.push(andFilter);
} else if (filterFields.length === 1) {
whereArg.value.fields.push(filterFields[0].fields[0]);
}
});
}
}
});
} }
/* eslint-enable no-loop-func */ /* eslint-enable no-loop-func */

View File

@@ -259,28 +259,30 @@
- active: - active:
_eq: true _eq: true
columns: columns:
- id - billid
- bodyshopid
- created - created
- operation - id
- jobid
- new_val - new_val
- old_val - old_val
- operation
- type
- useremail - useremail
- bodyshopid
- jobid
- billid
select_permissions: select_permissions:
- role: user - role: user
permission: permission:
columns: columns:
- billid
- bodyshopid
- created
- id - id
- jobid
- new_val - new_val
- old_val - old_val
- operation - operation
- type
- useremail - useremail
- created
- billid
- bodyshopid
- jobid
filter: filter:
bodyshop: bodyshop:
associations: associations:
@@ -4198,7 +4200,7 @@
interval_sec: 10 interval_sec: 10
num_retries: 0 num_retries: 0
timeout_sec: 60 timeout_sec: 60
webhook_from_env: HASURA_API_URL webhook: https://worktest.home.irony.online
headers: headers:
- name: event-secret - name: event-secret
value_from_env: EVENT_SECRET value_from_env: EVENT_SECRET

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."audit_trail" add column "type" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."audit_trail" add column "type" text
null;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."ioevents" add column "useremail" text
-- not null;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" add column "useremail" text;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."ioevents" add column "bodyshopid" uuid
-- null;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."ioevents" add column "env" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."ioevents" add column "env" text
null;

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."ioevents_useremail";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "ioevents_useremail" on
"public"."ioevents" using btree ("useremail");

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" drop constraint "ioevents_useremail_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."ioevents"
add constraint "ioevents_useremail_fkey"
foreign key ("useremail")
references "public"."users"
("email") on update set null on delete set null;

View File

@@ -0,0 +1 @@
alter table "public"."ioevents" drop constraint "ioevents_bodyshopid_fkey";

View File

@@ -0,0 +1,5 @@
alter table "public"."ioevents"
add constraint "ioevents_bodyshopid_fkey"
foreign key ("bodyshopid")
references "public"."bodyshops"
("id") on update set null on delete set null;

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."idx_audit_trail_type";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "idx_audit_trail_type" on
"public"."audit_trail" using btree ("type");

748
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@aws-sdk/client-secrets-manager": "^3.454.0", "@aws-sdk/client-secrets-manager": "^3.454.0",
"@aws-sdk/client-ses": "^3.454.0", "@aws-sdk/client-ses": "^3.454.0",
"@aws-sdk/credential-provider-node": "^3.451.0", "@aws-sdk/credential-provider-node": "^3.451.0",
"@azure/storage-blob": "^12.17.0",
"@opensearch-project/opensearch": "^2.4.0", "@opensearch-project/opensearch": "^2.4.0",
"aws4": "^1.12.0", "aws4": "^1.12.0",
"axios": "^1.6.2", "axios": "^1.6.2",
@@ -52,6 +53,7 @@
"xmlbuilder2": "^3.1.1" "xmlbuilder2": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"source-map-explorer": "^2.5.2" "source-map-explorer": "^2.5.2"
}, },
@@ -695,11 +697,496 @@
"tslib": "^2.3.1" "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", "version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"optional": true, "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": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
}, },
@@ -718,6 +1205,97 @@
"node": ">=6.9.0" "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": { "node_modules/@colors/colors": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", "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", "resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz",
"integrity": "sha512-bIcKnH5YmtTYr7S6J3J86dn/rFiklwRpOqbTOQ9C0WMmR9FKHVb3bxs2UYfqEmNb93O4nbA97sb6rtz33i9SyA==" "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": { "node_modules/@jsdoc/salty": {
"version": "0.2.6", "version": "0.2.6",
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz", "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz",
@@ -1106,6 +1732,14 @@
"yarn": "^1.22.10" "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": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1714,6 +2348,29 @@
"node": ">= 6" "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": { "node_modules/@types/body-parser": {
"version": "1.19.5", "version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -1857,6 +2514,15 @@
"undici-types": "~5.26.4" "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": { "node_modules/@types/qs": {
"version": "6.9.10", "version": "6.9.10",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", "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", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" "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": { "node_modules/@xmldom/xmldom": {
"version": "0.8.10", "version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -3307,6 +3981,14 @@
"node": ">=6" "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": { "node_modules/express": {
"version": "4.18.2", "version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -3854,6 +4536,15 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/google-auth-library": {
"version": "8.9.0", "version": "8.9.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz",
@@ -4446,6 +5137,12 @@
"node": "*" "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": { "node_modules/jose": {
"version": "4.15.4", "version": "4.15.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", "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", "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==" "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": { "node_modules/js-yaml": {
"version": "3.14.1", "version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
@@ -4540,6 +5243,18 @@
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
"optional": true "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": { "node_modules/json-2-csv": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.0.1.tgz",
@@ -5457,6 +6172,14 @@
"node": ">= 0.8.0" "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": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "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" "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": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -6949,6 +7681,14 @@
"node": ">=0.6.x" "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": { "node_modules/tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

View File

@@ -21,6 +21,7 @@
"@aws-sdk/client-secrets-manager": "^3.454.0", "@aws-sdk/client-secrets-manager": "^3.454.0",
"@aws-sdk/client-ses": "^3.454.0", "@aws-sdk/client-ses": "^3.454.0",
"@aws-sdk/credential-provider-node": "^3.451.0", "@aws-sdk/credential-provider-node": "^3.451.0",
"@azure/storage-blob": "^12.17.0",
"@opensearch-project/opensearch": "^2.4.0", "@opensearch-project/opensearch": "^2.4.0",
"aws4": "^1.12.0", "aws4": "^1.12.0",
"axios": "^1.6.2", "axios": "^1.6.2",
@@ -61,6 +62,7 @@
"xmlbuilder2": "^3.1.1" "xmlbuilder2": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"source-map-explorer": "^2.5.2" "source-map-explorer": "^2.5.2"
} }

View File

@@ -201,19 +201,24 @@ exports.default = async (req, res) => {
} finally { } finally {
sftp.end(); sftp.end();
} }
sendServerEmail({ // sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, // subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} // text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify( // Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), // allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null, // null,
2 // 2
)} // )}
`, // `,
}); // });
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (error) {
res.status(200).json(error); res.status(200).json(error);
sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY @ HH:mm:ss")}`,
text: `Errors: JSON.stringify(error)}
All Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}`,
});
} }
}; };

View File

@@ -11,27 +11,40 @@ require("dotenv").config({
}); });
exports.default = async (req, res) => { exports.default = async (req, res) => {
const { operationName, time, dbevent, user, imexshopid } = req.body; const {
useremail,
bodyshopid,
operationName,
variables,
env,
time,
dbevent,
user,
} = req.body;
try { try {
// await client.request(queries.INSERT_IOEVENT, { await client.request(queries.INSERT_IOEVENT, {
// event: { event: {
// operationname: operationName, operationname: operationName,
// time, time,
// dbevent, dbevent,
// }, env,
// }); variables,
console.log("IOEVENT", operationName, time, dbevent, user, imexshopid); bodyshopid,
logger.log("ioevent", "trace", user, null, { useremail,
imexshopid, },
operationName,
time,
dbevent,
}); });
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (error) {
console.log("error", error); logger.log("ioevent-error", "trace", user, null, {
res.status(400).send(error); operationname: operationName,
time,
dbevent,
env,
variables,
bodyshopid,
useremail,
});
res.sendStatus(200);
} }
}; };

View File

@@ -3,6 +3,7 @@ const queries = require("../graphql-client/queries");
const moment = require("moment"); const moment = require("moment");
const durationToHumanReadable = require("../utils/durationToHumanReadable"); const durationToHumanReadable = require("../utils/durationToHumanReadable");
const calculateStatusDuration = require("../utils/calculateStatusDuration"); const calculateStatusDuration = require("../utils/calculateStatusDuration");
const getLifecycleStatusColor = require("../utils/getLifecycleStatusColor");
const jobLifecycle = async (req, res) => { const jobLifecycle = async (req, res) => {
// Grab the jobids and statuses from the request body // Grab the jobids and statuses from the request body
@@ -28,12 +29,12 @@ const jobLifecycle = async (req, res) => {
jobIDs, jobIDs,
transitions: [] transitions: []
}); });
} }
const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const transitionsByJobId = _.groupBy(resp.transitions, 'jobid');
const groupedTransitions = {}; const groupedTransitions = {};
const allDurations = [];
for (let jobId in transitionsByJobId) { for (let jobId in transitionsByJobId) {
let lifecycle = transitionsByJobId[jobId].map(transition => { let lifecycle = transitionsByJobId[jobId].map(transition => {
@@ -53,15 +54,57 @@ const jobLifecycle = async (req, res) => {
return transition; return transition;
}); });
const durations = calculateStatusDuration(lifecycle, statuses);
groupedTransitions[jobId] = { groupedTransitions[jobId] = {
lifecycle: lifecycle, lifecycle,
durations: calculateStatusDuration(lifecycle, statuses), durations
}; };
if (durations?.summations) {
allDurations.push(durations.summations);
}
} }
const finalSummations = [];
const flatGroupedAllDurations = _.groupBy(allDurations.flat(),'status');
const finalStatusCounts = Object.keys(flatGroupedAllDurations).reduce((acc, status) => {
acc[status] = flatGroupedAllDurations[status].length;
return acc;
}, {});
// Calculate total value of all statuses
const finalTotal = Object.values(flatGroupedAllDurations).reduce((total, statusArr) => {
return total + statusArr.reduce((acc, curr) => acc + curr.value, 0);
}, 0);
Object.keys(flatGroupedAllDurations).forEach(status => {
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
const humanReadable = durationToHumanReadable(moment.duration(value));
const percentage = (value / finalTotal) * 100;
const color = getLifecycleStatusColor(status);
const roundedPercentage = `${Math.round(percentage)}%`;
finalSummations.push({
status,
value,
humanReadable,
percentage,
color,
roundedPercentage
});
});
return res.status(200).json({ return res.status(200).json({
jobIDs, jobIDs,
transition: groupedTransitions, transition: groupedTransitions,
durations: {
jobs: jobIDs.length,
summations: finalSummations,
totalStatuses: finalSummations.length,
total: finalTotal,
statusCounts: finalStatusCounts,
humanReadable: durationToHumanReadable(moment.duration(finalTotal))
}
}); });
} }

View File

@@ -1,15 +1,7 @@
const durationToHumanReadable = require("./durationToHumanReadable"); const durationToHumanReadable = require("./durationToHumanReadable");
const moment = require("moment"); const moment = require("moment");
const getLifecycleStatusColor = require("./getLifecycleStatusColor");
const _ = require("lodash"); const _ = require("lodash");
const crypto = require('crypto');
const getColor = (key) => {
const hash = crypto.createHash('sha256');
hash.update(key);
const hashedKey = hash.digest('hex');
const num = parseInt(hashedKey, 16);
return '#' + (num % 16777215).toString(16).padStart(6, '0');
};
const calculateStatusDuration = (transitions, statuses) => { const calculateStatusDuration = (transitions, statuses) => {
let statusDuration = {}; let statusDuration = {};
@@ -33,26 +25,16 @@ const calculateStatusDuration = (transitions, statuses) => {
if (!transition.prev_value) { if (!transition.prev_value) {
statusDuration[transition.value] = { statusDuration[transition.value] = {
value: duration, value: duration,
humanReadable: transition.duration_readable humanReadable: durationToHumanReadable(moment.duration(duration))
}; };
} else if (!transition.next_value) { } else {
if (statusDuration[transition.value]) { if (statusDuration[transition.value]) {
statusDuration[transition.value].value += duration; statusDuration[transition.value].value += duration;
statusDuration[transition.value].humanReadable = transition.duration_readable; statusDuration[transition.value].humanReadable = durationToHumanReadable(moment.duration(statusDuration[transition.value].value));
} else { } else {
statusDuration[transition.value] = { statusDuration[transition.value] = {
value: duration, value: duration,
humanReadable: transition.duration_readable humanReadable: durationToHumanReadable(moment.duration(duration))
};
}
} else {
if (statusDuration[transition.value]) {
statusDuration[transition.value].value += duration;
statusDuration[transition.value].humanReadable = transition.duration_readable;
} else {
statusDuration[transition.value] = {
value: duration,
humanReadable: transition.duration_readable
}; };
} }
} }
@@ -79,7 +61,7 @@ const calculateStatusDuration = (transitions, statuses) => {
value, value,
humanReadable, humanReadable,
percentage: statusDuration[status].percentage, percentage: statusDuration[status].percentage,
color: getColor(status), color: getLifecycleStatusColor(status),
roundedPercentage: `${Math.round(statusDuration[status].percentage)}%` roundedPercentage: `${Math.round(statusDuration[status].percentage)}%`
}); });
} }

View File

@@ -0,0 +1,11 @@
const crypto = require('crypto');
const getLifecycleStatusColor = (key) => {
const hash = crypto.createHash('sha256');
hash.update(key);
const hashedKey = hash.digest('hex');
const num = parseInt(hashedKey, 16);
return '#' + (num % 16777215).toString(16).padStart(6, '0');
};
module.exports = getLifecycleStatusColor;