Compare commits
73 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1ca09bd4f | ||
|
|
f9ca36ec89 | ||
|
|
f98c9e6f71 | ||
|
|
28f2e8ad30 | ||
|
|
c27e206687 | ||
|
|
01fd253f1d | ||
|
|
e67bc0d953 | ||
|
|
3eab3e2fb6 | ||
|
|
3adf6b649b | ||
|
|
f8243aa2b3 | ||
|
|
3c3f50d138 | ||
|
|
4f7e1b81ac | ||
|
|
806bdc4c70 | ||
|
|
a0572a0cec | ||
|
|
04cdf13e86 | ||
|
|
50349e91dc | ||
|
|
9998a8f154 | ||
|
|
9dcbcb2a43 | ||
|
|
5773f7a0f3 | ||
|
|
8614d88e71 | ||
|
|
fa5e26c52a | ||
|
|
90e1cbd390 | ||
|
|
947a3c6a88 | ||
|
|
a33662e6f0 | ||
|
|
6d1f04369e | ||
|
|
4a27726ef3 | ||
|
|
eedba97237 | ||
|
|
bcf095ed4f | ||
|
|
c3f9e268c7 | ||
|
|
c8442f0750 | ||
|
|
da5b446c30 | ||
|
|
e13b2bb969 | ||
|
|
e8969c4698 | ||
|
|
346d32a2bb | ||
|
|
6ba00a90be | ||
|
|
4293d20313 | ||
|
|
706c70c509 | ||
|
|
e872b1bf0a | ||
|
|
379fa060d8 | ||
|
|
a6258c6456 | ||
|
|
d6bf0a225b | ||
|
|
ec2b914e5e | ||
|
|
309a20148a | ||
|
|
013b56778b | ||
|
|
9dec4a3a61 | ||
|
|
189b4db90f | ||
|
|
c49fa1c527 | ||
|
|
53ef048f6f | ||
|
|
1cc7eed983 | ||
|
|
1c8f377212 | ||
|
|
c050947276 | ||
|
|
9bde1f820d | ||
|
|
393765640c | ||
|
|
69c2836425 | ||
|
|
ec9edf30eb | ||
|
|
6a812f9ea7 | ||
|
|
fa86254bfd | ||
|
|
58ab7afbb3 | ||
|
|
fa7d90d2a9 | ||
|
|
fbf9047974 | ||
|
|
3e9b795052 | ||
|
|
d29ffc21e5 | ||
|
|
959f7780e8 | ||
|
|
e47731702a | ||
|
|
85a3aeb335 | ||
|
|
80b7ae0e54 | ||
|
|
0529ac4478 | ||
|
|
3cafbebbee | ||
|
|
6f248d864e | ||
|
|
a4a84572b7 | ||
|
|
0bc00d46cf | ||
|
|
e80e40bb76 | ||
|
|
a88c102b27 |
16
.prettierrc.js
Normal file
16
.prettierrc.js
Normal file
@@ -0,0 +1,16 @@
|
||||
exports.default = {
|
||||
printWidth: 120,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
trailingComma: "es5",
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
bracketSpacing: true,
|
||||
arrowParens: "always",
|
||||
jsxSingleQuote: false,
|
||||
bracketSameLine: false,
|
||||
endOfLine: "lf",
|
||||
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
||||
importOrderSeparation: true,
|
||||
importOrderSortSpecifiers: true,
|
||||
};
|
||||
@@ -3,7 +3,13 @@
|
||||
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.
|
||||
|
||||
# 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
|
||||
|
||||
## 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.
|
||||
|
||||
## Reflection
|
||||
|
||||
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"name": "jobs.status",
|
||||
"translation": "jobs.fields.status",
|
||||
@@ -52,7 +59,7 @@ Filters can make use of reflection to pre-fill select boxes, the following is an
|
||||
"type": "internal",
|
||||
"name": "special.job_statuses"
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses`
|
||||
@@ -67,7 +74,13 @@ The following cases are available
|
||||
- `special.employees` - This will reflect the employees `bodyshop.employees`
|
||||
- `special.first_names` - This will reflect the first names `bodyshop.employees`
|
||||
- `special.last_names` - This will reflect the last names `bodyshop.employees`
|
||||
-
|
||||
- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources`
|
||||
- `special.class`- This will reflect the class `bodyshop.md_classes`
|
||||
- `special.lost_sale_reasons` - This will reflect the lost sale reasons `bodyshop.md_lost_sale_reasons`
|
||||
- `special.alt_transports` - This will reflect the alternative transports `bodyshop.appt_alt_transport`
|
||||
- `special.payment_types` - This will reflect the payment types `bodyshop.md_payment_types`
|
||||
- `special.payment_payers` - This is a special case with a key value set of [Customer, Insurance]
|
||||
|
||||
### Path without brackets, multi level
|
||||
|
||||
`"name": "jobs.joblines.mod_lb_hrs",`
|
||||
@@ -104,6 +117,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
|
||||
```
|
||||
|
||||
### Path with brackets,top level
|
||||
|
||||
`"name": "[jobs].joblines.mod_lb_hrs",`
|
||||
This will produce a where clause at the `jobs` level of the graphQL query.
|
||||
|
||||
@@ -138,18 +152,26 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
|
||||
```
|
||||
|
||||
## Known Caveats
|
||||
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
|
||||
- The `dates` object is not yet implemented and will be added in a future release.
|
||||
- The type object must be 'string' or 'number' and is case-sensitive.
|
||||
|
||||
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
|
||||
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
|
||||
- The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive.
|
||||
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
|
||||
- Do not add the ability to filter things that are already filtered as part of the original query, this would be redundant and could cause issues.
|
||||
- Do not add the ability to filter things that are already filtered as part of the original query, this would be
|
||||
redundant and could cause issues.
|
||||
- Do not add the ability to filter on things like FK constraints, must like the above example.
|
||||
- *INHERITANCE CAVEAT* If you have a filters file on a parent report that has a child that you do not want the filters inherited from, you must place a blank filters file (valid json so {}) on the child report level. This will than fetch the child filters, which are empty and move along, versus inheriting the parent filters.
|
||||
|
||||
## Sorters
|
||||
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level.
|
||||
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters.
|
||||
|
||||
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting,
|
||||
a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs`
|
||||
would be added at the `joblines` level.
|
||||
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results
|
||||
using the sorters.
|
||||
|
||||
### Default Sorters
|
||||
|
||||
- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
|
||||
- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.
|
||||
|
||||
@@ -164,4 +186,4 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
|
||||
"direction": "asc"
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,21 @@
|
||||
import { Input, Table, Checkbox, Card, Space } from "antd";
|
||||
import { Card, Checkbox, Input, Space, Table } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import queryString from "query-string";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -138,7 +138,6 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
|
||||
render: (text, record) => (
|
||||
<ExportLogsCountDisplay logs={record.exportlogs} />
|
||||
),
|
||||
@@ -147,8 +146,6 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
render: (text, record) => (
|
||||
<PayableExportButton
|
||||
billId={record.id}
|
||||
|
||||
@@ -8,14 +8,16 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
||||
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -75,7 +77,11 @@ export function AccountingPayablesTableComponent({
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
OwnerNameDisplayFunction(a.job),
|
||||
OwnerNameDisplayFunction(b.job)
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
@@ -94,6 +100,9 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("payments.fields.amount"),
|
||||
dataIndex: "amount",
|
||||
key: "amount",
|
||||
sorter: (a, b) => a.amount - b.amount,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||
),
|
||||
@@ -112,18 +121,21 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("payments.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.exportedat"),
|
||||
dataIndex: "exportedat",
|
||||
key: "exportedat",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// title: t("payments.fields.exportedat"),
|
||||
// dataIndex: "exportedat",
|
||||
// key: "exportedat",
|
||||
// render: (text, record) => (
|
||||
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
@@ -137,8 +149,6 @@ export function AccountingPayablesTableComponent({
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
render: (text, record) => (
|
||||
<PaymentExportButton
|
||||
paymentId={record.id}
|
||||
|
||||
@@ -4,17 +4,19 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -63,7 +65,7 @@ export function AccountingReceivablesTableComponent({
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => a.status - b.status,
|
||||
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -83,7 +85,8 @@ export function AccountingReceivablesTableComponent({
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
@@ -103,6 +106,15 @@ export function AccountingReceivablesTableComponent({
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||
|
||||
@@ -12,8 +12,8 @@ import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
|
||||
@@ -51,6 +51,7 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
||||
insertAuditTrail({
|
||||
jobid: jobid,
|
||||
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
|
||||
type: "billdeleted",
|
||||
});
|
||||
|
||||
if (callback && typeof callback === "function") callback(bill.id);
|
||||
|
||||
@@ -33,8 +33,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
@@ -150,6 +150,7 @@ export function BillDetailEditcontainer({
|
||||
jobid: bill.jobid,
|
||||
billid: search.billid,
|
||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||
type: "billupdated",
|
||||
});
|
||||
|
||||
await refetch();
|
||||
|
||||
@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||
insertAuditTrail: ({ jobid, billid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, billid, operation })),
|
||||
insertAuditTrail: ({ jobid, billid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, billid, operation, type })),
|
||||
});
|
||||
|
||||
const Templates = TemplateList("job_special");
|
||||
@@ -171,6 +171,7 @@ function BillEnterModalContainer({
|
||||
mod_lbr_ty: key,
|
||||
hours: adjustmentsToInsert[key].toFixed(1),
|
||||
}),
|
||||
type: "jobmodifylbradj",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -320,6 +321,7 @@ function BillEnterModalContainer({
|
||||
operation: AuditTrailMapping.billposted(
|
||||
r1.data.insert_bills.returning[0].invoice_number
|
||||
),
|
||||
type: "billposted",
|
||||
});
|
||||
|
||||
if (enterAgain) {
|
||||
|
||||
@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
||||
});
|
||||
|
||||
@@ -102,6 +102,7 @@ const CardPaymentModalComponent = ({
|
||||
insertAuditTrail({
|
||||
jobid: payment.jobid,
|
||||
operation: AuditTrailMapping.failedpayment(),
|
||||
type: "failedpayment",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -17,13 +17,18 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
});
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [filter, setFilter] = useLocalStorage(
|
||||
"filter_courtesy_cars_list",
|
||||
null
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
@@ -50,6 +55,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
filteredValue: filter?.status || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.status.in"),
|
||||
@@ -112,6 +118,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
dataIndex: "readiness",
|
||||
key: "readiness",
|
||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||
filteredValue: filter?.readiness || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("courtesycars.readiness.ready"),
|
||||
@@ -227,7 +234,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
setFilter(filters);
|
||||
};
|
||||
|
||||
const tableData = searchText
|
||||
|
||||
@@ -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
|
||||
} `;
|
||||
@@ -1,380 +1,391 @@
|
||||
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd";
|
||||
import Icon, {SyncOutlined} from "@ant-design/icons";
|
||||
import {gql, useMutation, useQuery} from "@apollo/client";
|
||||
import {Button, Dropdown, Menu, notification, PageHeader, Space} from "antd";
|
||||
import i18next from "i18next";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MdClose } from "react-icons/md";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import React, {useState} from "react";
|
||||
import {Responsive, WidthProvider} from "react-grid-layout";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {MdClose} from "react-icons/md";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
||||
import {UPDATE_DASHBOARD_LAYOUT} from "../../graphql/user.queries";
|
||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import DashboardMonthlyEmployeeEfficiency, {
|
||||
DashboardMonthlyEmployeeEfficiencyGql,
|
||||
DashboardMonthlyEmployeeEfficiencyGql,
|
||||
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.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 DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
|
||||
import DashboardMonthlyRevenueGraph, {
|
||||
DashboardMonthlyRevenueGraphGql,
|
||||
DashboardMonthlyRevenueGraphGql,
|
||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
||||
import DashboardProjectedMonthlySales, {
|
||||
DashboardProjectedMonthlySalesGql,
|
||||
DashboardProjectedMonthlySalesGql,
|
||||
} 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, {
|
||||
DashboardTotalProductionHoursGql,
|
||||
DashboardTotalProductionHoursGql,
|
||||
} from "../dashboard-components/total-production-hours/total-production-hours.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
//Combination of the following:
|
||||
// /node_modules/react-grid-layout/css/styles.css
|
||||
// /node_modules/react-resizable/css/styles.css
|
||||
import DashboardScheduledInToday, {
|
||||
DashboardScheduledInTodayGql,
|
||||
DashboardScheduledInTodayGql,
|
||||
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
|
||||
import DashboardScheduledOutToday, {
|
||||
DashboardScheduledOutTodayGql,
|
||||
DashboardScheduledOutTodayGql,
|
||||
} 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 { GenerateDashboardData } from "./dashboard-grid.utils";
|
||||
import {GenerateDashboardData} from "./dashboard-grid.utils";
|
||||
|
||||
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
...(bodyshop.associations[0].user.dashboardlayout
|
||||
? bodyshop.associations[0].user.dashboardlayout
|
||||
: { 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 },
|
||||
},
|
||||
export function DashboardGridComponent({currentUser, bodyshop}) {
|
||||
const {t} = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
...(bodyshop.associations[0].user.dashboardlayout
|
||||
? bodyshop.associations[0].user.dashboardlayout
|
||||
: {items: [], 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);
|
||||
setState({ ...state, items });
|
||||
};
|
||||
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||
|
||||
const handleAddComponent = (e) => {
|
||||
logImEXEvent("dashboard_add_component", { name: e });
|
||||
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 handleLayoutChange = async (layout, layouts) => {
|
||||
logImEXEvent("dashboard_change_layout");
|
||||
|
||||
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>
|
||||
);
|
||||
setState({...state, layout, layouts});
|
||||
|
||||
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>
|
||||
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);
|
||||
|
||||
<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}
|
||||
>
|
||||
{state.items.map((item, index) => {
|
||||
const TheComponent = componentList[item.i].component;
|
||||
return (
|
||||
<div
|
||||
key={item.i}
|
||||
data-grid={{
|
||||
...item,
|
||||
minH: componentList[item.i].minH || 1,
|
||||
minW: componentList[item.i].minW || 1,
|
||||
}}
|
||||
const items = _.cloneDeep(state.items);
|
||||
|
||||
items.splice(idxToRemove, 1);
|
||||
setState({...state, items});
|
||||
};
|
||||
|
||||
const handleAddComponent = (e) => {
|
||||
logImEXEvent("dashboard_add_component", {name: e});
|
||||
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(
|
||||
() => 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}>
|
||||
<Icon
|
||||
component={MdClose}
|
||||
key={item.i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: "2",
|
||||
right: ".25rem",
|
||||
top: ".25rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => handleRemoveComponent(item.i)}
|
||||
/>
|
||||
<TheComponent className="dashboard-card" data={dashboarddata} />
|
||||
</LoadingSkeleton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ResponsiveReactGridLayout>
|
||||
</div>
|
||||
);
|
||||
{state.items.map((item, index) => {
|
||||
const TheComponent = componentList[item.i].component;
|
||||
return (
|
||||
<div
|
||||
key={item.i}
|
||||
data-grid={{
|
||||
...item,
|
||||
minH: componentList[item.i].minH || 1,
|
||||
minW: componentList[item.i].minW || 1,
|
||||
}}
|
||||
>
|
||||
<LoadingSkeleton loading={loading}>
|
||||
<Icon
|
||||
component={MdClose}
|
||||
key={item.i}
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: "2",
|
||||
right: ".25rem",
|
||||
top: ".25rem",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => handleRemoveComponent(item.i)}
|
||||
/>
|
||||
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboarddata}/>
|
||||
</LoadingSkeleton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ResponsiveReactGridLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DashboardGridComponent);
|
||||
|
||||
const componentList = {
|
||||
ProductionDollars: {
|
||||
label: i18next.t("dashboard.titles.productiondollars"),
|
||||
component: DashboardTotalProductionDollars,
|
||||
gqlFragment: null,
|
||||
w: 1,
|
||||
h: 1,
|
||||
minW: 2,
|
||||
minH: 1,
|
||||
},
|
||||
ProductionHours: {
|
||||
label: i18next.t("dashboard.titles.productionhours"),
|
||||
component: DashboardTotalProductionHours,
|
||||
gqlFragment: DashboardTotalProductionHoursGql,
|
||||
w: 3,
|
||||
h: 1,
|
||||
minW: 3,
|
||||
minH: 1,
|
||||
},
|
||||
ProjectedMonthlySales: {
|
||||
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
||||
component: DashboardProjectedMonthlySales,
|
||||
gqlFragment: DashboardProjectedMonthlySalesGql,
|
||||
w: 2,
|
||||
h: 1,
|
||||
minW: 2,
|
||||
minH: 1,
|
||||
},
|
||||
MonthlyRevenueGraph: {
|
||||
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
||||
component: DashboardMonthlyRevenueGraph,
|
||||
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
||||
w: 4,
|
||||
h: 2,
|
||||
minW: 4,
|
||||
minH: 2,
|
||||
},
|
||||
MonthlyJobCosting: {
|
||||
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
||||
component: DashboardMonthlyJobCosting,
|
||||
gqlFragment: null,
|
||||
minW: 6,
|
||||
minH: 3,
|
||||
w: 6,
|
||||
h: 3,
|
||||
},
|
||||
MonthlyPartsSales: {
|
||||
label: i18next.t("dashboard.titles.monthlypartssales"),
|
||||
component: DashboardMonthlyPartsSales,
|
||||
gqlFragment: null,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
MonthlyLaborSales: {
|
||||
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
||||
component: DashboardMonthlyLaborSales,
|
||||
gqlFragment: null,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
MonthlyEmployeeEfficency: {
|
||||
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
||||
component: DashboardMonthlyEmployeeEfficiency,
|
||||
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
ScheduleInToday: {
|
||||
label: i18next.t("dashboard.titles.scheduledintoday"),
|
||||
component: DashboardScheduledInToday,
|
||||
gqlFragment: DashboardScheduledInTodayGql,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
w: 10,
|
||||
h: 3,
|
||||
},
|
||||
ScheduleOutToday: {
|
||||
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
||||
component: DashboardScheduledOutToday,
|
||||
gqlFragment: DashboardScheduledOutTodayGql,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
w: 10,
|
||||
h: 3,
|
||||
},
|
||||
ProductionDollars: {
|
||||
label: i18next.t("dashboard.titles.productiondollars"),
|
||||
component: DashboardTotalProductionDollars,
|
||||
gqlFragment: null,
|
||||
w: 1,
|
||||
h: 1,
|
||||
minW: 2,
|
||||
minH: 1,
|
||||
},
|
||||
ProductionHours: {
|
||||
label: i18next.t("dashboard.titles.productionhours"),
|
||||
component: DashboardTotalProductionHours,
|
||||
gqlFragment: DashboardTotalProductionHoursGql,
|
||||
w: 3,
|
||||
h: 1,
|
||||
minW: 3,
|
||||
minH: 1,
|
||||
},
|
||||
ProjectedMonthlySales: {
|
||||
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
||||
component: DashboardProjectedMonthlySales,
|
||||
gqlFragment: DashboardProjectedMonthlySalesGql,
|
||||
w: 2,
|
||||
h: 1,
|
||||
minW: 2,
|
||||
minH: 1,
|
||||
},
|
||||
MonthlyRevenueGraph: {
|
||||
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
||||
component: DashboardMonthlyRevenueGraph,
|
||||
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
||||
w: 4,
|
||||
h: 2,
|
||||
minW: 4,
|
||||
minH: 2,
|
||||
},
|
||||
MonthlyJobCosting: {
|
||||
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
||||
component: DashboardMonthlyJobCosting,
|
||||
gqlFragment: null,
|
||||
minW: 6,
|
||||
minH: 3,
|
||||
w: 6,
|
||||
h: 3,
|
||||
},
|
||||
MonthlyPartsSales: {
|
||||
label: i18next.t("dashboard.titles.monthlypartssales"),
|
||||
component: DashboardMonthlyPartsSales,
|
||||
gqlFragment: null,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
MonthlyLaborSales: {
|
||||
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
||||
component: DashboardMonthlyLaborSales,
|
||||
gqlFragment: null,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
|
||||
MonthlyEmployeeEfficency: {
|
||||
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
||||
component: DashboardMonthlyEmployeeEfficiency,
|
||||
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
ScheduleInToday: {
|
||||
label: i18next.t("dashboard.titles.scheduledintoday"),
|
||||
component: DashboardScheduledInToday,
|
||||
gqlFragment: DashboardScheduledInTodayGql,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
w: 10,
|
||||
h: 3,
|
||||
},
|
||||
ScheduleOutToday: {
|
||||
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
||||
component: DashboardScheduledOutToday,
|
||||
gqlFragment: DashboardScheduledOutTodayGql,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
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 componentBasedAdditions =
|
||||
state &&
|
||||
Array.isArray(state.layout) &&
|
||||
state.layout
|
||||
.map((item, index) => componentList[item.i].gqlFragment || "")
|
||||
.join("");
|
||||
return gql`
|
||||
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
||||
monthly_sales: jobs(where: {_and: [
|
||||
{ voided: {_eq: false}},
|
||||
{date_invoiced: {_gte: "${moment()
|
||||
const componentBasedAdditions =
|
||||
state &&
|
||||
Array.isArray(state.layout) &&
|
||||
state.layout
|
||||
.map((item, index) => componentList[item.i].gqlFragment || "")
|
||||
.join("");
|
||||
return gql`
|
||||
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
||||
monthly_sales: jobs(where: {_and: [
|
||||
{ voided: {_eq: false}},
|
||||
{date_invoiced: {_gte: "${moment()
|
||||
.startOf("month")
|
||||
.startOf("day")
|
||||
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
|
||||
.startOf("day")
|
||||
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}]}) {
|
||||
id
|
||||
ro_number
|
||||
date_invoiced
|
||||
job_totals
|
||||
rate_la1
|
||||
rate_la2
|
||||
rate_la3
|
||||
rate_la4
|
||||
rate_laa
|
||||
rate_lab
|
||||
rate_lad
|
||||
rate_lae
|
||||
rate_laf
|
||||
rate_lag
|
||||
rate_lam
|
||||
rate_lar
|
||||
rate_las
|
||||
rate_lau
|
||||
rate_ma2s
|
||||
rate_ma2t
|
||||
rate_ma3s
|
||||
rate_mabl
|
||||
rate_macs
|
||||
rate_mahw
|
||||
rate_mapa
|
||||
rate_mash
|
||||
rate_matd
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
}
|
||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
ro_number
|
||||
ins_co_nm
|
||||
job_totals
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
ro_number
|
||||
date_invoiced
|
||||
job_totals
|
||||
rate_la1
|
||||
rate_la2
|
||||
rate_la3
|
||||
rate_la4
|
||||
rate_laa
|
||||
rate_lab
|
||||
rate_lad
|
||||
rate_lae
|
||||
rate_laf
|
||||
rate_lag
|
||||
rate_lam
|
||||
rate_lar
|
||||
rate_las
|
||||
rate_lau
|
||||
rate_ma2s
|
||||
rate_ma2t
|
||||
rate_ma3s
|
||||
rate_mabl
|
||||
rate_macs
|
||||
rate_mahw
|
||||
rate_mapa
|
||||
rate_mash
|
||||
rate_matd
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
}
|
||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
ro_number
|
||||
ins_co_nm
|
||||
job_totals
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
};
|
||||
|
||||
@@ -53,6 +53,7 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
|
||||
insertAuditTrail({
|
||||
jobid: event.job.id,
|
||||
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||
type: "appointmentcancel",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobChecklistForm({
|
||||
@@ -183,6 +183,7 @@ export function JobChecklistForm({
|
||||
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
|
||||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
|
||||
),
|
||||
type: "jobchecklist",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
|
||||
@@ -18,10 +18,8 @@ export default function JobDetailCardsTotalsComponent({ loading, data }) {
|
||||
/>
|
||||
<Statistic
|
||||
className="imex-flex-row__margin-large"
|
||||
title={t("jobs.fields.ded_amt")}
|
||||
value={Dinero({
|
||||
amount: Math.round((data.ded_amt || 0) * 100),
|
||||
}).toFormat()}
|
||||
title={t("jobs.fields.customerowing")}
|
||||
value={Dinero(data.job_totals.totals.custPayable.total).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
className="imex-flex-row__margin-large"
|
||||
|
||||
@@ -23,7 +23,6 @@ export function JobEmployeeAssignments({
|
||||
jobRO,
|
||||
body,
|
||||
refinish,
|
||||
|
||||
prep,
|
||||
csr,
|
||||
handleAdd,
|
||||
@@ -78,7 +77,7 @@ export function JobEmployeeAssignments({
|
||||
setVisibility(false);
|
||||
}}
|
||||
>
|
||||
Assign
|
||||
{t("allocations.actions.assign")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||
</Space>
|
||||
|
||||
@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -43,12 +43,13 @@ export function JobEmployeeAssignmentsContainer({
|
||||
});
|
||||
if (refetch) refetch();
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobassignmentchange(operation, name),
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
if (!!!result.errors) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobassignmentchange(operation, name),
|
||||
type: "jobassignmentchange",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.assigning", {
|
||||
message: JSON.stringify(result.errors),
|
||||
@@ -66,17 +67,19 @@ export function JobEmployeeAssignmentsContainer({
|
||||
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"]({
|
||||
message: t("jobs.errors.assigning", {
|
||||
message: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobassignmentremoved(operation),
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -113,6 +113,7 @@ export function JobLineConvertToLabor({
|
||||
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
|
||||
mod_lbr_ty,
|
||||
}),
|
||||
type: "jobmodifylbradj",
|
||||
});
|
||||
setLoading(false);
|
||||
setVisibility(false);
|
||||
|
||||
@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
|
||||
|
||||
@@ -32,6 +32,7 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobstatuschange(status),
|
||||
type: "admin_jobstatuschange",
|
||||
});
|
||||
// refetch();
|
||||
})
|
||||
|
||||
@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
@@ -57,6 +57,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
||||
? DateTimeFormat(changedAuditFields[key])
|
||||
: changedAuditFields[key]
|
||||
),
|
||||
type: "admin_jobfieldchange",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -59,6 +59,7 @@ export function JobAdminMarkReexport({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobmarkforreexport(),
|
||||
type: "admin_jobmarkforreexport",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
@@ -99,6 +100,7 @@ export function JobAdminMarkReexport({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobmarkexported(),
|
||||
type: "admin_jobmarkexported",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
@@ -124,6 +126,7 @@ export function JobAdminMarkReexport({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobuninvoice(),
|
||||
type: "admin_jobuninvoice",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
|
||||
@@ -10,8 +10,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
|
||||
@@ -34,6 +34,7 @@ export function JobsAdminRemoveAR({ insertAuditTrail, job }) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_job_remove_from_ar(value),
|
||||
type: "admin_job_remove_from_ar",
|
||||
});
|
||||
setSwitchValue(value);
|
||||
} else {
|
||||
|
||||
@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
|
||||
|
||||
@@ -49,6 +49,7 @@ export function JobsAdminUnvoid({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobunvoid(),
|
||||
type: "admin_jobunvoid",
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
|
||||
@@ -47,8 +47,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export function JobsAvailableContainer({
|
||||
bodyshop,
|
||||
@@ -190,6 +190,7 @@ export function JobsAvailableContainer({
|
||||
insertAuditTrail({
|
||||
jobid: r.data.insert_jobs.returning[0].id,
|
||||
operation: AuditTrailMapping.jobimported(),
|
||||
type: "jobimported",
|
||||
});
|
||||
|
||||
deleteJob({
|
||||
@@ -350,6 +351,7 @@ export function JobsAvailableContainer({
|
||||
insertAuditTrail({
|
||||
jobid: selectedJob,
|
||||
operation: AuditTrailMapping.jobsupplement(),
|
||||
type: "jobsupplement",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
@@ -35,6 +35,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(status),
|
||||
type: "jobstatuschange",
|
||||
});
|
||||
// refetch();
|
||||
})
|
||||
|
||||
@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
function updateJobCache(items) {
|
||||
@@ -192,6 +192,7 @@ export function JobsCloseExportButton({
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
|
||||
@@ -217,6 +218,7 @@ export function JobsCloseExportButton({
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
updateJobCache([
|
||||
...new Set(
|
||||
|
||||
@@ -25,8 +25,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsConvertButton({
|
||||
@@ -78,6 +78,7 @@ export function JobsConvertButton({
|
||||
operation: AuditTrailMapping.jobconverted(
|
||||
res.data.update_jobs.returning[0].ro_number
|
||||
),
|
||||
type: "jobconverted",
|
||||
});
|
||||
|
||||
setVisible(false);
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function AddToProduction(
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobinproductionchange(!remove),
|
||||
type: "jobinproductionchange",
|
||||
})
|
||||
);
|
||||
if (completionCallback) completionCallback();
|
||||
|
||||
@@ -52,8 +52,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
setCardPaymentContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "cardPayment" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsDetailHeaderActions({
|
||||
@@ -115,6 +115,7 @@ export function JobsDetailHeaderActions({
|
||||
? !job.production_vars.alert
|
||||
: true
|
||||
),
|
||||
type: "alertToggle",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -134,6 +135,7 @@ export function JobsDetailHeaderActions({
|
||||
operation: AuditTrailMapping.jobsuspend(
|
||||
!!job.suspended ? !job.suspended : true
|
||||
),
|
||||
type: "jobsuspend",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -190,6 +192,7 @@ export function JobsDetailHeaderActions({
|
||||
jobid: job.id,
|
||||
operation:
|
||||
AuditTrailMapping.appointmentcancel(lost_sale_reason),
|
||||
type: "appointmentcancel",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -549,6 +552,7 @@ export function JobsDetailHeaderActions({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobvoid(),
|
||||
type: "jobvoid",
|
||||
});
|
||||
//go back to jobs list.
|
||||
history.push(`/manage/`);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
PauseCircleOutlined,
|
||||
WarningFilled,
|
||||
} 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 { useTranslation } from "react-i18next";
|
||||
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 VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
import "./jobs-detail-header.styles.scss";
|
||||
import moment from "moment";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -62,6 +63,13 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
${job.v_make_desc || ""}
|
||||
${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();
|
||||
|
||||
return (
|
||||
@@ -93,7 +101,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
{job.status === bodyshop.md_ro_statuses.default_scheduled &&
|
||||
job.scheduled_in ? (
|
||||
<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>
|
||||
) : null}
|
||||
</Space>
|
||||
@@ -299,6 +309,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
>
|
||||
<div>
|
||||
<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>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
function updateJobCache(items) {
|
||||
@@ -189,6 +189,7 @@ export function JobsExportAllButton({
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
});
|
||||
updateJobCache(
|
||||
@@ -230,6 +231,7 @@ export function JobsExportAllButton({
|
||||
insertAuditTrail({
|
||||
jobid: successfulTransactionsSet[0],
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
}
|
||||
updateJobCache(successfulTransactionsSet);
|
||||
|
||||
@@ -21,6 +21,7 @@ import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -145,7 +146,8 @@ export function JobsList({ bodyshop }) {
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
@@ -188,7 +190,8 @@ export function JobsList({ bodyshop }) {
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sorter: (a, b) =>
|
||||
statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filteredValue: filter?.status || null,
|
||||
@@ -219,6 +222,15 @@ export function JobsList({ bodyshop }) {
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
@@ -266,6 +278,9 @@ export function JobsList({ bodyshop }) {
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
|
||||
filteredValue: filter?.ins_co_nm || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
@@ -302,6 +317,13 @@ export function JobsList({ bodyshop }) {
|
||||
key: "estimator",
|
||||
ellipsis: true,
|
||||
responsive: ["xl"],
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(),
|
||||
`${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim()
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order,
|
||||
filterSearch: true,
|
||||
filteredValue: filter?.estimator || null,
|
||||
filters:
|
||||
|
||||
@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer);
|
||||
|
||||
@@ -49,6 +49,7 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobnotedeleted(),
|
||||
type: "jobnotedeleted",
|
||||
});
|
||||
});
|
||||
setDeleteLoading(false);
|
||||
|
||||
@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
@@ -76,6 +76,7 @@ export function LaborAllocationsAdjustmentEdit({
|
||||
values.hours -
|
||||
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
|
||||
}),
|
||||
type: "jobmodifylbradj",
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function NoteUpsertModalContainer({
|
||||
@@ -70,6 +70,7 @@ export function NoteUpsertModalContainer({
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteupdated(),
|
||||
type: "jobnoteupdated",
|
||||
});
|
||||
});
|
||||
if (refetch) refetch();
|
||||
@@ -102,6 +103,7 @@ export function NoteUpsertModalContainer({
|
||||
insertAuditTrail({
|
||||
jobid: newJobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
type: "jobnoteadded",
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -115,6 +117,7 @@ export function NoteUpsertModalContainer({
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
type: "jobnoteadded",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
@@ -44,6 +44,15 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "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) =>
|
||||
record.vehicleid ? (
|
||||
<Link to={`/manage/vehicles/${record.vehicleid}`}>
|
||||
@@ -67,9 +76,15 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "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:
|
||||
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),
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useMutation, useQuery, useApolloClient } from "@apollo/client";
|
||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Form, Modal, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent, auth } from "../../firebase/firebase.utils";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import {
|
||||
INSERT_NEW_PARTS_ORDERS,
|
||||
QUERY_PARTS_ORDER_OEC,
|
||||
@@ -29,10 +33,6 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import PartsOrderModalComponent from "./parts-order-modal.component";
|
||||
import axios from "axios";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import _ from "lodash";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -45,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function PartsOrderModalContainer({
|
||||
@@ -142,6 +142,7 @@ export function PartsOrderModalContainer({
|
||||
: AuditTrailMapping.jobspartsorder(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
),
|
||||
type: isReturn ? "jobspartsreturn" : "jobspartsorder",
|
||||
});
|
||||
|
||||
const jobLinesResult = await updateJobLines({
|
||||
|
||||
@@ -95,15 +95,13 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
history.push({
|
||||
search: queryString.stringify({
|
||||
...searchParams,
|
||||
selected: record.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (record?.id) {
|
||||
history.replace({
|
||||
search: queryString.stringify({
|
||||
...searchParams,
|
||||
selected: record.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -350,6 +348,13 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
selectedRowKeys: [selected],
|
||||
type: "radio",
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleOnRowClick(record);
|
||||
},
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
await insertPayment({
|
||||
variables: {
|
||||
paymentInput: {
|
||||
amount: -refund_response.data.amount,
|
||||
transactionid: payment_response.response.receiptelements.transid,
|
||||
amount: -refund_response?.data?.amount,
|
||||
transactionid: payment_response?.response?.receiptelements?.transid,
|
||||
payer: record.payer,
|
||||
type: "Refund",
|
||||
jobid: payment_response.jobid,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Button, Form, Input, PageHeader, Space } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectAuthLevel,
|
||||
selectBodyshop,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import FormItemEmail from "../form-items-formatted/email-form-item.component";
|
||||
import PhoneFormItem, {
|
||||
@@ -8,12 +14,6 @@ import PhoneFormItem, {
|
||||
} from "../form-items-formatted/phone-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectAuthLevel,
|
||||
selectBodyshop,
|
||||
} from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
authLevel: selectAuthLevel,
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -46,13 +46,19 @@ export function PhonebookFormComponent({
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={`${form.getFieldValue("firstname") || ""} ${
|
||||
form.getFieldValue("lastname") || ""
|
||||
}${
|
||||
form.getFieldValue("company")
|
||||
? ` - ${form.getFieldValue("company")}`
|
||||
: ""
|
||||
}`}
|
||||
title={
|
||||
<Form.Item shouldUpdate>
|
||||
{() =>
|
||||
`${form.getFieldValue("firstname") || ""} ${
|
||||
form.getFieldValue("lastname") || ""
|
||||
}${
|
||||
form.getFieldValue("company")
|
||||
? ` - ${form.getFieldValue("company")}`
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
|
||||
@@ -29,8 +29,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionBoardKanbanComponent({
|
||||
@@ -133,6 +133,7 @@ export function ProductionBoardKanbanComponent({
|
||||
insertAuditTrail({
|
||||
jobid: card.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
|
||||
type: "jobstatuschange",
|
||||
});
|
||||
|
||||
if (update.errors) {
|
||||
|
||||
@@ -13,8 +13,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionListColumnAlert({ record, insertAuditTrail }) {
|
||||
@@ -46,6 +46,7 @@ export function ProductionListColumnAlert({ record, insertAuditTrail }) {
|
||||
? !record.production_vars.alert
|
||||
: true
|
||||
),
|
||||
type: "alertToggle",
|
||||
}).then(() => {
|
||||
if (record.refetch) record.refetch();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Space, Tooltip } from "antd";
|
||||
import { Checkbox, Space, Tooltip } from "antd";
|
||||
import i18n from "i18next";
|
||||
import moment from "moment";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { TimeFormatter } from "../../utils/DateFormatter";
|
||||
@@ -10,7 +9,9 @@ import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
import ProductionListColumnAlert from "./production-list-columns.alert.component";
|
||||
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
|
||||
@@ -84,7 +85,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</Link>
|
||||
),
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -95,8 +97,10 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
a.v_make_desc + a.v_model_desc,
|
||||
b.v_make_desc + b.v_model_desc
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
@@ -185,17 +189,12 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
|
||||
state.sortedInfo.columnKey === "date_next_contact" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
record.date_next_contact &&
|
||||
moment(record.date_next_contact).isBefore(moment())
|
||||
? "red"
|
||||
: "",
|
||||
}}
|
||||
>
|
||||
<ProductionListDate record={record} field="date_next_contact" time />
|
||||
</span>
|
||||
<ProductionListDate
|
||||
record={record}
|
||||
field="date_next_contact"
|
||||
pastIndicator
|
||||
time
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -291,6 +290,20 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
|
||||
dataIndex: "special_coverage_policy",
|
||||
key: "special_coverage_policy",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
Number(a.special_coverage_policy) - Number(b.special_coverage_policy),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "special_coverage_policy" &&
|
||||
state.sortedInfo.order,
|
||||
filters: [
|
||||
{ text: "True", value: true },
|
||||
{ text: "False", value: false },
|
||||
],
|
||||
onFilter: (value, record) =>
|
||||
value.includes(record.special_coverage_policy),
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.special_coverage_policy} />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -302,6 +315,16 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(bodyshop &&
|
||||
bodyshop.appt_alt_transport.map((s) => {
|
||||
return {
|
||||
text: s,
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
{record.alt_transport}
|
||||
@@ -382,7 +405,11 @@ const r = ({ technician, state, activeStatuses, data, bodyshop }) => {
|
||||
title: i18n.t("production.labels.alert"),
|
||||
dataIndex: "alert",
|
||||
key: "alert",
|
||||
|
||||
sorter: (a, b) =>
|
||||
Number(a.production_vars?.alert || false) -
|
||||
Number(b.production_vars?.alert || false),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alert" && state.sortedInfo.order,
|
||||
render: (text, record) => <ProductionListColumnAlert record={record} />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,12 +3,12 @@ import { useMutation } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
notification,
|
||||
Popover,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
notification,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -25,8 +25,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ProductionListEmpAssignment({
|
||||
@@ -55,6 +55,7 @@ export function ProductionListEmpAssignment({
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobassignmentchange(empAssignment, name),
|
||||
type: "jobassignmentchange",
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
@@ -80,6 +81,7 @@ export function ProductionListEmpAssignment({
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobassignmentremoved(empAssignment),
|
||||
type: "jobassignmentremoved",
|
||||
});
|
||||
|
||||
if (!!result.errors) {
|
||||
|
||||
@@ -12,8 +12,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export function ProductionListColumnCategory({ record, bodyshop }) {
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
|
||||
@@ -5,16 +5,16 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export function ProductionListColumnStatus({
|
||||
record,
|
||||
@@ -40,6 +40,7 @@ export function ProductionListColumnStatus({
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(key),
|
||||
type: "jobstatuschange",
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
@@ -6,7 +6,7 @@ import {useTranslation} from "react-i18next";
|
||||
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import {generateInternalReflections} from "./report-center-modal-utils";
|
||||
|
||||
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
|
||||
|
||||
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
|
||||
return (
|
||||
@@ -33,7 +33,7 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
return (
|
||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
|
||||
<Form.List name={["filters"]}>
|
||||
{(fields, {add, remove, move}) => {
|
||||
{(fields, {add, remove}) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
@@ -70,7 +70,9 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<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"]);
|
||||
@@ -80,7 +82,6 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
key={`${index}operator`}
|
||||
label={t('reportcenter.labels.advanced_filters_filter_operator')}
|
||||
name={[field.name, "operator"]}
|
||||
dependencies={[]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -90,19 +91,32 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
>
|
||||
<Select
|
||||
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>
|
||||
</Col>
|
||||
<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 type = filters.find(f => f.name === name)?.type;
|
||||
const reflector = filters.find(f => f.name === name)?.reflector;
|
||||
const operator = form.getFieldValue(['filters', field.name, "operator"]);
|
||||
const operatorType = operator ? getWhereOperatorsByType(type).find((o) => o.value === operator)?.type : null;
|
||||
|
||||
return <Form.Item
|
||||
key={`${index}value`}
|
||||
@@ -128,14 +142,29 @@ function FiltersSection({filters, form, bodyshop}) {
|
||||
return generateInternalReflections({
|
||||
bodyshop,
|
||||
upperPath,
|
||||
finalPath
|
||||
finalPath,
|
||||
t
|
||||
});
|
||||
};
|
||||
|
||||
const reflections = reflector ? generateReflections(reflector) : [];
|
||||
const fieldPath = [[field.name, "value"]];
|
||||
|
||||
// We have reflections so we will use a select box
|
||||
if (reflections.length > 0) {
|
||||
// We have reflections and the operator type is array, so we will use a select box with multiple options
|
||||
if (operatorType === "array") {
|
||||
return (
|
||||
<Select
|
||||
disabled={!operator}
|
||||
mode="multiple"
|
||||
options={reflections}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue(fieldPath, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
options={reflections}
|
||||
@@ -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") {
|
||||
return (
|
||||
<InputNumber
|
||||
disabled={!operator}
|
||||
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
|
||||
);
|
||||
}
|
||||
|
||||
// We have a type of date, so we will use a date picker
|
||||
if (type === "date") {
|
||||
return (
|
||||
<FormDatePicker
|
||||
disabled={!operator}
|
||||
onChange={(date) => form.setFieldValue(fieldPath, date)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// we have a type of boolean, so we will use a select box with a true or false option.
|
||||
if (type === "boolean" || type === "bool") {
|
||||
return (
|
||||
<Select
|
||||
disabled={!operator}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
options={[
|
||||
{
|
||||
label: t('reportcenter.labels.advanced_filters_true'),
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: t('reportcenter.labels.advanced_filters_false'),
|
||||
value: false
|
||||
}
|
||||
]}
|
||||
onChange={(value) => form.setFieldValue(fieldPath, value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
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}
|
||||
* @constructor
|
||||
*/
|
||||
function SortersSection({sorters, form}) {
|
||||
function SortersSection({sorters}) {
|
||||
const {t} = useTranslation();
|
||||
return (
|
||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
|
||||
<Form.List name={["sorters"]}>
|
||||
{(fields, {add, remove, move}) => {
|
||||
{(fields, {add, remove}) => {
|
||||
return (
|
||||
<div>
|
||||
Sorters
|
||||
|
||||
@@ -8,6 +8,20 @@ import {uniqBy} from "lodash";
|
||||
*/
|
||||
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
|
||||
|
||||
/**
|
||||
* Generate options from array
|
||||
* @param bodyshop
|
||||
* @param path
|
||||
* @returns {unknown[]}
|
||||
*/
|
||||
const generateOptionsFromArray = (bodyshop, path) => {
|
||||
const options = getValueFromPath(bodyshop, path);
|
||||
return uniqBy(options.map((value) => ({
|
||||
label: value,
|
||||
value: value,
|
||||
})), 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid internal reflections
|
||||
* Note: This is intended for future functionality
|
||||
@@ -42,19 +56,44 @@ const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
|
||||
* Generate special reflections
|
||||
* @param bodyshop
|
||||
* @param finalPath
|
||||
* @param t - i18n
|
||||
* @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
|
||||
*/
|
||||
const generateSpecialReflections = (bodyshop, finalPath) => {
|
||||
const generateSpecialReflections = (bodyshop, finalPath, t) => {
|
||||
switch (finalPath) {
|
||||
case 'payment_payers':
|
||||
return [
|
||||
{
|
||||
label: t("payments.labels.customer"),
|
||||
value: t("payments.labels.customer"),
|
||||
},
|
||||
{
|
||||
label: t("payments.labels.insurance"),
|
||||
value: t("payments.labels.insurance"),
|
||||
},
|
||||
// This is a weird one supposedly only used by one shop and could potentially be
|
||||
// placed behind a SplitSDK
|
||||
{
|
||||
label: t("payments.labels.external"),
|
||||
value: t("payments.labels.external"),
|
||||
}
|
||||
];
|
||||
case 'payment_types':
|
||||
return generateOptionsFromArray(bodyshop, 'md_payment_types');
|
||||
case 'alt_transports':
|
||||
return generateOptionsFromArray(bodyshop, 'appt_alt_transport');
|
||||
case 'lost_sale_reasons':
|
||||
return generateOptionsFromArray(bodyshop, 'md_lost_sale_reasons');
|
||||
// Special case because Referral Sources is an Array, not an Object.
|
||||
case 'referral_source':
|
||||
return generateOptionsFromArray(bodyshop, 'md_referral_sources');
|
||||
case 'class':
|
||||
return generateOptionsFromArray(bodyshop, 'md_classes');
|
||||
case 'cost_centers':
|
||||
return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
|
||||
// Special case because Categories is an Array, not an Object.
|
||||
case 'categories':
|
||||
const catOptions = getValueFromPath(bodyshop, 'md_categories');
|
||||
return uniqBy(catOptions.map((value) => ({
|
||||
label: value,
|
||||
value: value,
|
||||
})), 'value');
|
||||
return generateOptionsFromArray(bodyshop, 'md_categories');
|
||||
case 'insurance_companies':
|
||||
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
|
||||
case 'employee_teams':
|
||||
@@ -105,12 +144,13 @@ const generateBodyshopReflections = (bodyshop, finalPath) => {
|
||||
* @param bodyshop
|
||||
* @param upperPath
|
||||
* @param finalPath
|
||||
* @param t - i18n
|
||||
* @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
|
||||
*/
|
||||
const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
|
||||
const generateInternalReflections = ({bodyshop, upperPath, finalPath, t}) => {
|
||||
switch (upperPath) {
|
||||
case 'special':
|
||||
return generateSpecialReflections(bodyshop, finalPath);
|
||||
return generateSpecialReflections(bodyshop, finalPath, t);
|
||||
case 'bodyshop':
|
||||
return generateBodyshopReflections(bodyshop, finalPath);
|
||||
default:
|
||||
@@ -118,4 +158,4 @@ const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
|
||||
}
|
||||
};
|
||||
|
||||
export {generateInternalReflections,}
|
||||
export {generateInternalReflections}
|
||||
@@ -34,8 +34,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function ScheduleJobModalContainer({
|
||||
@@ -146,6 +146,7 @@ export function ScheduleJobModalContainer({
|
||||
operation: AuditTrailMapping.appointmentinsert(
|
||||
DateTimeFormat(values.start)
|
||||
),
|
||||
type: "appointmentinsert",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
|
||||
return acc + value.bodyhrs;
|
||||
}, 0);
|
||||
|
||||
const numJobs = entries.length;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={moment(date).format("D - ddd")}
|
||||
@@ -33,17 +35,18 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
|
||||
>
|
||||
<Statistic
|
||||
valueStyle={{ color: dailyBodyTarget > bodyHrs ? "red" : "green" }}
|
||||
label="B"
|
||||
label="Body"
|
||||
value={bodyHrs.toFixed(1)}
|
||||
/>
|
||||
<Statistic
|
||||
valueStyle={{ color: dailyPaintTarget > paintHrs ? "red" : "green" }}
|
||||
label="P"
|
||||
label="Refinish"
|
||||
value={paintHrs.toFixed(1)}
|
||||
/>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
||||
<Statistic value={(bodyHrs + paintHrs).toFixed(1)} />
|
||||
<Statistic label="Total" value={(bodyHrs + paintHrs).toFixed(1)} />
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Statistic label="Jobs" value={numJobs} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,10 +29,13 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
let ret = {
|
||||
todayBody: 0,
|
||||
todayPaint: 0,
|
||||
todayJobs: 0,
|
||||
weeklyPaint: 0,
|
||||
weeklyJobs: 0,
|
||||
weeklyBody: 0,
|
||||
toDateBody: 0,
|
||||
toDatePaint: 0,
|
||||
toDateJobs: 0,
|
||||
};
|
||||
|
||||
const today = moment();
|
||||
@@ -40,6 +43,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.todayBody = ret.todayBody + d.bodyhrs;
|
||||
ret.todayPaint = ret.todayPaint + d.painthrs;
|
||||
ret.todayJobs++;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,6 +53,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
|
||||
ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
|
||||
ret.weeklyJobs++;
|
||||
});
|
||||
}
|
||||
StartOfWeek = StartOfWeek.add(1, "day");
|
||||
@@ -60,6 +65,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
|
||||
ret.toDateBody = ret.toDateBody + d.bodyhrs;
|
||||
ret.toDatePaint = ret.toDatePaint + d.painthrs;
|
||||
ret.toDateJobs++;
|
||||
});
|
||||
}
|
||||
startOfMonth = startOfMonth.add(1, "day");
|
||||
@@ -87,7 +93,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
<Statistic
|
||||
title={t("scoreboard.labels.dailytarget")}
|
||||
value={bodyshop.scoreboard_target.dailyBodyTarget}
|
||||
prefix="B"
|
||||
prefix={t("scoreboard.labels.bodyabbrev")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
@@ -140,7 +146,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={bodyshop.scoreboard_target.dailyPaintTarget}
|
||||
prefix="P"
|
||||
prefix={t("scoreboard.labels.refinishabbrev")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
@@ -181,7 +187,12 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
<Divider style={{ margin: 5 }} />
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}></Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={"\u00A0"}
|
||||
prefix={t("scoreboard.labels.total")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={(values.todayPaint + values.todayBody).toFixed(1)}
|
||||
@@ -240,6 +251,29 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Divider style={{ margin: 5 }} />
|
||||
</Row>
|
||||
<Row>
|
||||
<Col {...statSpans}>
|
||||
<Statistic
|
||||
value={"\u00A0"}
|
||||
prefix={t("scoreboard.labels.jobs")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.todayJobs} />
|
||||
</Col>
|
||||
<Col {...statSpans} />
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.weeklyJobs} />
|
||||
</Col>
|
||||
<Col {...statSpans} />
|
||||
<Col {...statSpans} />
|
||||
<Col {...statSpans}>
|
||||
<Statistic value={values.toDateJobs} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Typography,
|
||||
} from "antd";
|
||||
@@ -17,6 +18,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
|
||||
const SelectorDiv = styled.div`
|
||||
.ant-form-item .ant-select {
|
||||
@@ -191,7 +193,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("bodyshop.labels.dms.cdk.payers")}>
|
||||
<Form.List name={["cdk_configuration", "payers"]}>
|
||||
{(fields, { add, remove }) => {
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
@@ -249,11 +251,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<Space align="center">
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
/>
|
||||
</Space>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
))}
|
||||
@@ -345,7 +354,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
||||
id="costs"
|
||||
>
|
||||
<Form.List name={["md_responsibility_centers", "costs"]}>
|
||||
{(fields, { add, remove }) => {
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
@@ -462,12 +471,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
||||
<Input onBlur={handleBlur} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<Space align="center">
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
/>
|
||||
</Space>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
))}
|
||||
@@ -493,7 +508,7 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
||||
id="profits"
|
||||
>
|
||||
<Form.List name={["md_responsibility_centers", "profits"]}>
|
||||
{(fields, { add, remove }) => {
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
@@ -595,11 +610,18 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
||||
<Input onBlur={handleBlur} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<Space align="center">
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows
|
||||
move={move}
|
||||
index={index}
|
||||
total={fields.length}
|
||||
/>
|
||||
</Space>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
))}
|
||||
|
||||
@@ -7,7 +7,9 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
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";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -45,6 +47,10 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/owners/${record.owner.id}`}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
@@ -63,9 +69,15 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "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:
|
||||
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),
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -4,8 +4,9 @@ import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { alphaSort } from './../../utils/sorters';
|
||||
export default function VehiclesListComponent({
|
||||
loading,
|
||||
vehicles,
|
||||
@@ -31,6 +32,8 @@ export default function VehiclesListComponent({
|
||||
title: t("vehicles.fields.v_vin"),
|
||||
dataIndex: "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) => (
|
||||
<Link to={"/manage/vehicles/" + record.id}>
|
||||
<VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay>
|
||||
@@ -51,8 +54,10 @@ export default function VehiclesListComponent({
|
||||
},
|
||||
{
|
||||
title: t("vehicles.fields.plate_no"),
|
||||
dataIndex: "plate",
|
||||
key: "plate",
|
||||
dataIndex: "plate_no",
|
||||
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) => {
|
||||
return (
|
||||
<span>{`${record.plate_st || ""} | ${record.plate_no || ""}`}</span>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { getAuth, updatePassword, updateProfile } from "firebase/auth";
|
||||
import { getFirestore } from "firebase/firestore";
|
||||
import { getMessaging, getToken, onMessage } from "firebase/messaging";
|
||||
import { store } from "../redux/store";
|
||||
import axios from "axios";
|
||||
import { checkBeta } from "../utils/handleBeta";
|
||||
|
||||
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
|
||||
initializeApp(config);
|
||||
@@ -86,6 +88,18 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
null,
|
||||
...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(
|
||||
// "%c[Analytics]",
|
||||
// "background-color: green ;font-weight:bold;",
|
||||
|
||||
@@ -40,6 +40,7 @@ export const INSERT_AUDIT_TRAIL = gql`
|
||||
bodyshopid
|
||||
created
|
||||
operation
|
||||
type
|
||||
useremail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1862,6 +1862,7 @@ export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownerid
|
||||
plate_no
|
||||
plate_st
|
||||
v_vin
|
||||
|
||||
@@ -31,6 +31,7 @@ export const QUERY_VEHICLE_BY_ID = gql`
|
||||
jobs(order_by: { date_open: desc }) {
|
||||
id
|
||||
ro_number
|
||||
ownr_co_nm
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
owner {
|
||||
|
||||
@@ -40,8 +40,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||
@@ -127,6 +127,7 @@ export function DmsContainer({
|
||||
insertAuditTrail({
|
||||
jobid: payload,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported",
|
||||
});
|
||||
history.push("/manage/accounting/receivables");
|
||||
});
|
||||
|
||||
@@ -12,7 +12,8 @@ import AlertComponent from "../../components/alert/alert.component";
|
||||
import { QUERY_EXPORT_LOG_PAGINATED } from "../../graphql/accounting.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "./../../utils/sorters";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -34,11 +35,43 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
limit: pageLimit,
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
...(sortcolumn === "ro_number"
|
||||
? {
|
||||
job: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
}
|
||||
: sortcolumn === "invoice_number"
|
||||
? {
|
||||
bill: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
}
|
||||
: sortcolumn === "paymentnum"
|
||||
? {
|
||||
payment: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
}
|
||||
: {
|
||||
[sortcolumn || "created_at"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -68,6 +101,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("general.labels.created_at"),
|
||||
dataIndex: "created_at",
|
||||
key: "created_at",
|
||||
sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
||||
sortOrder: sortcolumn === "created_at" && sortorder,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||
),
|
||||
@@ -81,7 +116,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
render: (text, record) =>
|
||||
record.job && (
|
||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||
@@ -93,6 +129,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("bills.fields.invoice_number"),
|
||||
dataIndex: "invoice_number",
|
||||
key: "invoice_number",
|
||||
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
|
||||
sortOrder: sortcolumn === "invoice_number" && sortorder,
|
||||
render: (text, record) =>
|
||||
record.bill && (
|
||||
<Link to={"/manage/bills?billid=" + (record.bill && record.bill.id)}>
|
||||
@@ -104,6 +142,8 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("payments.fields.paymentnum"),
|
||||
dataIndex: "paymentnum",
|
||||
key: "paymentnum",
|
||||
sorter: (a, b) => alphaSort(a.paymentnum, b.paymentnum),
|
||||
sortOrder: sortcolumn === "paymentnum" && sortorder,
|
||||
render: (text, record) =>
|
||||
record.payment && (
|
||||
<Link
|
||||
@@ -120,6 +160,13 @@ export function ExportLogsPageComponent({ bodyshop }) {
|
||||
title: t("general.labels.successful"),
|
||||
dataIndex: "successful",
|
||||
key: "successful",
|
||||
sorter: (a, b) => Number(a.successful) - Number(b.successful),
|
||||
sortOrder: sortcolumn === "successful" && sortorder,
|
||||
filters: [
|
||||
{ text: "True", value: true },
|
||||
{ text: "False", value: false },
|
||||
],
|
||||
onFilter: (value, record) => record.successful === value,
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.successful} />
|
||||
),
|
||||
|
||||
@@ -47,11 +47,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
|
||||
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, }) {
|
||||
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const client = useApolloClient();
|
||||
@@ -121,6 +121,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, })
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobinvoiced(),
|
||||
type: "jobinvoiced",
|
||||
});
|
||||
// history.push(`/manage/jobs/${job.id}`);
|
||||
} else {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
|
||||
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
|
||||
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
|
||||
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
|
||||
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
|
||||
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
|
||||
@@ -54,7 +55,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import { DateTimeFormat } from "./../../utils/DateFormatter";
|
||||
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -63,8 +63,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPrintCenterContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
});
|
||||
export function JobsDetailPage({
|
||||
bodyshop,
|
||||
@@ -177,6 +177,7 @@ export function JobsDetailPage({
|
||||
? DateTimeFormat(changedAuditFields[key])
|
||||
: changedAuditFields[key]
|
||||
),
|
||||
type: "jobfieldchange",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -334,15 +335,23 @@ export function JobsDetailPage({
|
||||
>
|
||||
<JobsDetailLaborContainer job={job} jobId={job.id} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
forceRender
|
||||
tab={<span><BarsOutlined />{t('menus.jobsdetail.lifecycle')}</span>}
|
||||
key="lifecycle"
|
||||
>
|
||||
<JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses}/>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
forceRender
|
||||
tab={
|
||||
<span>
|
||||
<BarsOutlined />
|
||||
{t("menus.jobsdetail.lifecycle")}
|
||||
</span>
|
||||
}
|
||||
key="lifecycle"
|
||||
>
|
||||
<JobLifecycleComponent
|
||||
job={job}
|
||||
statuses={bodyshop.md_ro_statuses}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane
|
||||
<Tabs.TabPane
|
||||
forceRender
|
||||
tab={
|
||||
<span>
|
||||
|
||||
@@ -54,9 +54,9 @@ export const setOnline = (isOnline) => ({
|
||||
payload: isOnline,
|
||||
});
|
||||
|
||||
export const insertAuditTrail = ({ jobid, billid, operation }) => ({
|
||||
export const insertAuditTrail = ({ jobid, billid, operation, type }) => ({
|
||||
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
|
||||
payload: { jobid, billid, operation },
|
||||
payload: { jobid, billid, operation, type },
|
||||
});
|
||||
export const setProblemJobs = (problemJobs) => ({
|
||||
type: ApplicationActionTypes.SET_PROBLEM_JOBS,
|
||||
|
||||
@@ -266,7 +266,7 @@ export function* onInsertAuditTrail() {
|
||||
}
|
||||
|
||||
export function* insertAuditTrailSaga({
|
||||
payload: { jobid, billid, operation },
|
||||
payload: { jobid, billid, operation, type },
|
||||
}) {
|
||||
const state = yield select();
|
||||
const bodyshop = state.user.bodyshop;
|
||||
@@ -278,6 +278,7 @@ export function* insertAuditTrailSaga({
|
||||
jobid,
|
||||
billid,
|
||||
operation,
|
||||
type,
|
||||
useremail: currentUser.email,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
"saving": "Error encountered while saving. {{message}}"
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
|
||||
"address1": "Address 1",
|
||||
"address2": "Address 2",
|
||||
"appt_alt_transport": "Appointment Alternative Transportation Options",
|
||||
@@ -477,7 +478,6 @@
|
||||
"editaccess": "Users -> Edit access"
|
||||
}
|
||||
},
|
||||
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
|
||||
"responsibilitycenter": "Responsibility Center",
|
||||
"responsibilitycenter_accountdesc": "Account Description",
|
||||
"responsibilitycenter_accountitem": "Item",
|
||||
@@ -608,7 +608,7 @@
|
||||
"dms": {
|
||||
"cdk": {
|
||||
"controllist": "Control Number List",
|
||||
"payers": "CDK Payers"
|
||||
"payers": "Payers"
|
||||
},
|
||||
"cdk_dealerid": "CDK Dealer ID",
|
||||
"pbs_serialnumber": "PBS Serial Number",
|
||||
@@ -844,8 +844,8 @@
|
||||
"notconfigured": "You do not have any current CSI Question Sets configured.",
|
||||
"notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.",
|
||||
"notfoundtitle": "No survey found.",
|
||||
"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": {
|
||||
"completedon": "Completed On",
|
||||
@@ -854,13 +854,13 @@
|
||||
"validuntil": "Valid Until"
|
||||
},
|
||||
"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_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.",
|
||||
"noneselected": "No response selected.",
|
||||
"title": "Customer Satisfaction Survey",
|
||||
"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."
|
||||
"title": "Customer Satisfaction Survey"
|
||||
},
|
||||
"successes": {
|
||||
"created": "CSI created successfully.",
|
||||
@@ -898,7 +898,8 @@
|
||||
"scheduledindate": "Sheduled In Today: {{date}}",
|
||||
"scheduledintoday": "Sheduled In Today",
|
||||
"scheduledoutdate": "Sheduled Out Today: {{date}}",
|
||||
"scheduledouttoday": "Sheduled Out Today"
|
||||
"scheduledouttoday": "Sheduled Out Today",
|
||||
"joblifecycle": "Job Lifecycle"
|
||||
}
|
||||
},
|
||||
"dms": {
|
||||
@@ -1236,7 +1237,15 @@
|
||||
"relative_end": "Relative End",
|
||||
"relative_start": "Relative 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": {
|
||||
"current_status_accumulated_time": "Current Status Accumulated Time",
|
||||
@@ -1248,7 +1257,9 @@
|
||||
"title": "Job Lifecycle Component",
|
||||
"title_durations": "Historical Status Durations",
|
||||
"title_loading": "Loading",
|
||||
"title_transitions": "Transitions"
|
||||
"title_transitions": "Transitions",
|
||||
"calculated_based_on": "Calculated based on",
|
||||
"jobs_in_since": "Jobs in since"
|
||||
},
|
||||
"errors": {
|
||||
"fetch": "Error getting Job Lifecycle Data"
|
||||
@@ -1823,6 +1834,7 @@
|
||||
"job": "Job Details",
|
||||
"jobcosting": "Job Costing",
|
||||
"jobtotals": "Job Totals",
|
||||
"labor_hrs": "B/P/T Hrs",
|
||||
"labor_rates_subtotal": "Labor Rates Subtotal",
|
||||
"laborallocations": "Labor Allocations",
|
||||
"labortotals": "Labor Totals",
|
||||
@@ -2417,6 +2429,7 @@
|
||||
"invoice_total_payable": "Invoice (Total Payable)",
|
||||
"iou_form": "IOU Form",
|
||||
"job_costing_ro": "Job Costing",
|
||||
"job_lifecycle_ro": "Job Lifecycle",
|
||||
"job_notes": "Job Notes",
|
||||
"key_tag": "Key Tag",
|
||||
"labels": {
|
||||
@@ -2583,15 +2596,17 @@
|
||||
},
|
||||
"labels": {
|
||||
"advanced_filters": "Advanced Filters and Sorters",
|
||||
"advanced_filters_show": "Show",
|
||||
"advanced_filters_hide": "Hide",
|
||||
"advanced_filters_filters": "Filters",
|
||||
"advanced_filters_sorters": "Sorters",
|
||||
"advanced_filters_false": "False",
|
||||
"advanced_filters_filter_field": "Field",
|
||||
"advanced_filters_sorter_field": "Field",
|
||||
"advanced_filters_sorter_direction": "Direction",
|
||||
"advanced_filters_filter_operator": "Operator",
|
||||
"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",
|
||||
"employee": "Employee",
|
||||
"filterson": "Filters on {{object}}: {{field}}",
|
||||
@@ -2673,6 +2688,8 @@
|
||||
"job_costing_ro_date_summary": "Job Costing by RO - Summary",
|
||||
"job_costing_ro_estimator": "Job Costing by Estimator",
|
||||
"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_invoiced_not_exported": "Jobs Invoiced not Exported",
|
||||
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
|
||||
@@ -2762,6 +2779,7 @@
|
||||
"allemployeetimetickets": "All Employee Time Tickets",
|
||||
"asoftodaytarget": "As of Today",
|
||||
"body": "Body",
|
||||
"bodyabbrev": "B",
|
||||
"bodycharttitle": "Body Targets vs Actual",
|
||||
"calendarperiod": "Periods based on calendar weeks/months.",
|
||||
"combinedcharttitle": "Combined Targets vs Actual",
|
||||
@@ -2778,6 +2796,7 @@
|
||||
"productivestatistics": "Productive Hours Statistics",
|
||||
"productivetimeticketsoverdate": "Productive Hours over Selected Dates",
|
||||
"refinish": "Refinish",
|
||||
"refinishabbrev": "R",
|
||||
"refinishcharttitle": "Refinish Targets vs Actual",
|
||||
"targets": "Targets",
|
||||
"thismonth": "This Month",
|
||||
@@ -2785,6 +2804,7 @@
|
||||
"timetickets": "Time Tickets",
|
||||
"timeticketsemployee": "Time Tickets by Employee",
|
||||
"todateactual": "Actual (MTD)",
|
||||
"total": "Total",
|
||||
"totalhrs": "Total Hours",
|
||||
"totaloverperiod": "Total over Selected Dates",
|
||||
"weeklyactual": "Actual (W)",
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
"saving": ""
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "",
|
||||
"address1": "",
|
||||
"address2": "",
|
||||
"appt_alt_transport": "",
|
||||
@@ -477,7 +478,6 @@
|
||||
"editaccess": ""
|
||||
}
|
||||
},
|
||||
"ReceivableCustomField": "",
|
||||
"responsibilitycenter": "",
|
||||
"responsibilitycenter_accountdesc": "",
|
||||
"responsibilitycenter_accountitem": "",
|
||||
@@ -844,8 +844,8 @@
|
||||
"notconfigured": "",
|
||||
"notfoundsubtitle": "",
|
||||
"notfoundtitle": "",
|
||||
"surveycompletetitle": "",
|
||||
"surveycompletesubtitle": ""
|
||||
"surveycompletesubtitle": "",
|
||||
"surveycompletetitle": ""
|
||||
},
|
||||
"fields": {
|
||||
"completedon": "",
|
||||
@@ -854,13 +854,13 @@
|
||||
"validuntil": ""
|
||||
},
|
||||
"labels": {
|
||||
"copyright": "",
|
||||
"greeting": "",
|
||||
"intro": "",
|
||||
"nologgedinuser": "",
|
||||
"nologgedinuser_sub": "",
|
||||
"noneselected": "",
|
||||
"title": "",
|
||||
"greeting": "",
|
||||
"intro": "",
|
||||
"copyright": ""
|
||||
"title": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -898,7 +898,8 @@
|
||||
"scheduledindate": "",
|
||||
"scheduledintoday": "",
|
||||
"scheduledoutdate": "",
|
||||
"scheduledouttoday": ""
|
||||
"scheduledouttoday": "",
|
||||
"joblifecycle": ""
|
||||
}
|
||||
},
|
||||
"dms": {
|
||||
@@ -1236,7 +1237,15 @@
|
||||
"relative_end": "",
|
||||
"relative_start": "",
|
||||
"start": "",
|
||||
"value": ""
|
||||
"value": "",
|
||||
"status": "",
|
||||
"percentage": "",
|
||||
"human_readable": "",
|
||||
"status_count": ""
|
||||
},
|
||||
"titles": {
|
||||
"dashboard": "",
|
||||
"top_durations": ""
|
||||
},
|
||||
"content": {
|
||||
"current_status_accumulated_time": "",
|
||||
@@ -1248,7 +1257,9 @@
|
||||
"title": "",
|
||||
"title_durations": "",
|
||||
"title_loading": "",
|
||||
"title_transitions": ""
|
||||
"title_transitions": "",
|
||||
"calculated_based_on": "",
|
||||
"jobs_in_since": ""
|
||||
},
|
||||
"errors": {
|
||||
"fetch": "Error al obtener los datos del ciclo de vida del trabajo"
|
||||
@@ -1823,6 +1834,7 @@
|
||||
"job": "",
|
||||
"jobcosting": "",
|
||||
"jobtotals": "",
|
||||
"labor_hrs": "",
|
||||
"labor_rates_subtotal": "",
|
||||
"laborallocations": "",
|
||||
"labortotals": "",
|
||||
@@ -2417,6 +2429,7 @@
|
||||
"invoice_total_payable": "",
|
||||
"iou_form": "",
|
||||
"job_costing_ro": "",
|
||||
"job_lifecycle_ro": "",
|
||||
"job_notes": "",
|
||||
"key_tag": "",
|
||||
"labels": {
|
||||
@@ -2583,15 +2596,17 @@
|
||||
},
|
||||
"labels": {
|
||||
"advanced_filters": "",
|
||||
"advanced_filters_show": "",
|
||||
"advanced_filters_hide": "",
|
||||
"advanced_filters_filters": "",
|
||||
"advanced_filters_sorters": "",
|
||||
"advanced_filters_false": "",
|
||||
"advanced_filters_filter_field": "",
|
||||
"advanced_filters_sorter_field": "",
|
||||
"advanced_filters_sorter_direction": "",
|
||||
"advanced_filters_filter_operator": "",
|
||||
"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": "",
|
||||
"employee": "",
|
||||
"filterson": "",
|
||||
@@ -2673,6 +2688,8 @@
|
||||
"job_costing_ro_date_summary": "",
|
||||
"job_costing_ro_estimator": "",
|
||||
"job_costing_ro_ins_co": "",
|
||||
"job_lifecycle_date_detail": "",
|
||||
"job_lifecycle_date_summary": "",
|
||||
"jobs_completed_not_invoiced": "",
|
||||
"jobs_invoiced_not_exported": "",
|
||||
"jobs_reconcile": "",
|
||||
@@ -2762,6 +2779,7 @@
|
||||
"allemployeetimetickets": "",
|
||||
"asoftodaytarget": "",
|
||||
"body": "",
|
||||
"bodyabbrev": "",
|
||||
"bodycharttitle": "",
|
||||
"calendarperiod": "",
|
||||
"combinedcharttitle": "",
|
||||
@@ -2778,6 +2796,7 @@
|
||||
"productivestatistics": "",
|
||||
"productivetimeticketsoverdate": "",
|
||||
"refinish": "",
|
||||
"refinishabbrev": "",
|
||||
"refinishcharttitle": "",
|
||||
"targets": "",
|
||||
"thismonth": "",
|
||||
@@ -2785,6 +2804,7 @@
|
||||
"timetickets": "",
|
||||
"timeticketsemployee": "",
|
||||
"todateactual": "",
|
||||
"total": "",
|
||||
"totalhrs": "",
|
||||
"totaloverperiod": "",
|
||||
"weeklyactual": "",
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
"saving": ""
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "",
|
||||
"address1": "",
|
||||
"address2": "",
|
||||
"appt_alt_transport": "",
|
||||
@@ -477,7 +478,6 @@
|
||||
"editaccess": ""
|
||||
}
|
||||
},
|
||||
"ReceivableCustomField": "",
|
||||
"responsibilitycenter": "",
|
||||
"responsibilitycenter_accountdesc": "",
|
||||
"responsibilitycenter_accountitem": "",
|
||||
@@ -844,8 +844,8 @@
|
||||
"notconfigured": "",
|
||||
"notfoundsubtitle": "",
|
||||
"notfoundtitle": "",
|
||||
"surveycompletetitle": "",
|
||||
"surveycompletesubtitle": ""
|
||||
"surveycompletesubtitle": "",
|
||||
"surveycompletetitle": ""
|
||||
},
|
||||
"fields": {
|
||||
"completedon": "",
|
||||
@@ -854,13 +854,13 @@
|
||||
"validuntil": ""
|
||||
},
|
||||
"labels": {
|
||||
"copyright": "",
|
||||
"greeting": "",
|
||||
"intro": "",
|
||||
"nologgedinuser": "",
|
||||
"nologgedinuser_sub": "",
|
||||
"noneselected": "",
|
||||
"title": "",
|
||||
"greeting": "",
|
||||
"intro": "",
|
||||
"copyright": ""
|
||||
"title": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -1236,7 +1236,15 @@
|
||||
"relative_end": "",
|
||||
"relative_start": "",
|
||||
"start": "",
|
||||
"value": ""
|
||||
"value": "",
|
||||
"status": "",
|
||||
"percentage": "",
|
||||
"human_readable": "",
|
||||
"status_count": ""
|
||||
},
|
||||
"titles": {
|
||||
"dashboard": "",
|
||||
"top_durations": ""
|
||||
},
|
||||
"content": {
|
||||
"current_status_accumulated_time": "",
|
||||
@@ -1248,7 +1256,10 @@
|
||||
"title": "",
|
||||
"title_durations": "",
|
||||
"title_loading": "",
|
||||
"title_transitions": ""
|
||||
"title_transitions": "",
|
||||
"calculated_based_on": "",
|
||||
"jobs_in_since": "",
|
||||
"joblifecycle": ""
|
||||
},
|
||||
"errors": {
|
||||
"fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches"
|
||||
@@ -1823,6 +1834,7 @@
|
||||
"job": "",
|
||||
"jobcosting": "",
|
||||
"jobtotals": "",
|
||||
"labor_hrs": "",
|
||||
"labor_rates_subtotal": "",
|
||||
"laborallocations": "",
|
||||
"labortotals": "",
|
||||
@@ -2417,6 +2429,7 @@
|
||||
"invoice_total_payable": "",
|
||||
"iou_form": "",
|
||||
"job_costing_ro": "",
|
||||
"job_lifecycle_ro": "",
|
||||
"job_notes": "",
|
||||
"key_tag": "",
|
||||
"labels": {
|
||||
@@ -2583,15 +2596,17 @@
|
||||
},
|
||||
"labels": {
|
||||
"advanced_filters": "",
|
||||
"advanced_filters_show": "",
|
||||
"advanced_filters_hide": "",
|
||||
"advanced_filters_filters": "",
|
||||
"advanced_filters_sorters": "",
|
||||
"advanced_filters_false": "",
|
||||
"advanced_filters_filter_field": "",
|
||||
"advanced_filters_sorter_field": "",
|
||||
"advanced_filters_sorter_direction": "",
|
||||
"advanced_filters_filter_operator": "",
|
||||
"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": "",
|
||||
"employee": "",
|
||||
"filterson": "",
|
||||
@@ -2673,6 +2688,8 @@
|
||||
"job_costing_ro_date_summary": "",
|
||||
"job_costing_ro_estimator": "",
|
||||
"job_costing_ro_ins_co": "",
|
||||
"job_lifecycle_date_detail": "",
|
||||
"job_lifecycle_date_summary": "",
|
||||
"jobs_completed_not_invoiced": "",
|
||||
"jobs_invoiced_not_exported": "",
|
||||
"jobs_reconcile": "",
|
||||
@@ -2762,6 +2779,7 @@
|
||||
"allemployeetimetickets": "",
|
||||
"asoftodaytarget": "",
|
||||
"body": "",
|
||||
"bodyabbrev": "",
|
||||
"bodycharttitle": "",
|
||||
"calendarperiod": "",
|
||||
"combinedcharttitle": "",
|
||||
@@ -2778,6 +2796,7 @@
|
||||
"productivestatistics": "",
|
||||
"productivetimeticketsoverdate": "",
|
||||
"refinish": "",
|
||||
"refinishabbrev": "",
|
||||
"refinishcharttitle": "",
|
||||
"targets": "",
|
||||
"thismonth": "",
|
||||
@@ -2785,6 +2804,7 @@
|
||||
"timetickets": "",
|
||||
"timeticketsemployee": "",
|
||||
"todateactual": "",
|
||||
"total": "",
|
||||
"totalhrs": "",
|
||||
"totaloverperiod": "",
|
||||
"weeklyactual": "",
|
||||
|
||||
@@ -514,6 +514,14 @@ export const TemplateList = (type, context) => {
|
||||
group: "financial",
|
||||
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"
|
||||
@@ -2048,6 +2056,30 @@ export const TemplateList = (type, context) => {
|
||||
datedisable: true,
|
||||
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"
|
||||
|
||||
@@ -2,22 +2,66 @@ import {Kind, parse, print, visit} from "graphql";
|
||||
import client from "./GraphQLClient";
|
||||
import {gql} from "@apollo/client";
|
||||
|
||||
/* eslint-disable no-loop-func */
|
||||
|
||||
/**
|
||||
* The available operators for filtering (string)
|
||||
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
|
||||
*/
|
||||
const STRING_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_like", label: "contains"},
|
||||
{value: "_nlike", label: "does not contain"},
|
||||
{value: "_ilike", label: "contains case-insensitive"},
|
||||
{value: "_nilike", label: "does not contain case-insensitive"}
|
||||
{value: "_nilike", label: "does not contain case-insensitive"},
|
||||
{value: "_in", label: "in", type: "array"},
|
||||
{value: "_nin", label: "not in", type: "array"}
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for filtering (dates)
|
||||
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
|
||||
*/
|
||||
const DATE_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_gt", label: "greater than"},
|
||||
{value: "_lt", label: "less than"},
|
||||
{value: "_gte", label: "greater than or equal"},
|
||||
{value: "_lte", label: "less than or equal"},
|
||||
{value: "_in", label: "in", type: "array"},
|
||||
{value: "_nin", label: "not in", type: "array"}
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for filtering (booleans)
|
||||
* @type {[{label: string, value: string},{label: string, value: string}]}
|
||||
*/
|
||||
const BOOLEAN_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for filtering (numbers)
|
||||
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
|
||||
*/
|
||||
const NUMBER_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_gt", label: "greater than"},
|
||||
{value: "_lt", label: "less than"},
|
||||
{value: "_gte", label: "greater than or equal"},
|
||||
{value: "_lte", label: "less than or equal"}
|
||||
{value: "_lte", label: "less than or equal"},
|
||||
{value: "_in", label: "in", type: "array"},
|
||||
{value: "_nin", label: "not in", type: "array"}
|
||||
];
|
||||
|
||||
/**
|
||||
* The available operators for sorting
|
||||
* @type {[{label: string, value: string},{label: string, value: string}]}
|
||||
*/
|
||||
const ORDER_BY_OPERATORS = [
|
||||
{value: "asc", label: "ascending"},
|
||||
{value: "desc", label: "descending"}
|
||||
@@ -31,7 +75,6 @@ export function getOrderOperatorsByType() {
|
||||
return ORDER_BY_OPERATORS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the available operators for filtering
|
||||
* @param type
|
||||
@@ -40,13 +83,14 @@ export function getOrderOperatorsByType() {
|
||||
export function getWhereOperatorsByType(type = 'string') {
|
||||
const operators = {
|
||||
string: STRING_OPERATORS,
|
||||
number: NUMBER_OPERATORS
|
||||
number: NUMBER_OPERATORS,
|
||||
boolean: BOOLEAN_OPERATORS,
|
||||
bool: BOOLEAN_OPERATORS,
|
||||
date: DATE_OPERATORS
|
||||
};
|
||||
return operators[type];
|
||||
}
|
||||
|
||||
/* eslint-disable no-loop-func */
|
||||
|
||||
/**
|
||||
* Parse a GraphQL query into an AST
|
||||
* @param query
|
||||
@@ -78,11 +122,9 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
|
||||
// Parse the query and apply the filters and sorters
|
||||
const ast = parseQuery(templateQueryToExecute);
|
||||
|
||||
let filterFields = [];
|
||||
|
||||
if (templateObject?.filters && templateObject?.filters?.length) {
|
||||
applyFilters(ast, templateObject.filters, filterFields);
|
||||
wrapFiltersInAnd(ast, filterFields);
|
||||
applyFilters(ast, templateObject.filters);
|
||||
}
|
||||
|
||||
if (templateObject?.sorters && templateObject?.sorters?.length) {
|
||||
@@ -109,7 +151,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
|
||||
return {contextData, useShopSpecificTemplate};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply sorters to the AST
|
||||
* @param ast
|
||||
@@ -149,16 +190,16 @@ export function applySorters(ast, sorters) {
|
||||
if (!orderByArg) {
|
||||
orderByArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: { kind: Kind.NAME, value: 'order_by' },
|
||||
value: { kind: Kind.OBJECT, fields: [] },
|
||||
name: {kind: Kind.NAME, value: 'order_by'},
|
||||
value: {kind: Kind.OBJECT, fields: []},
|
||||
};
|
||||
currentSelection.arguments.push(orderByArg);
|
||||
}
|
||||
|
||||
const sorterField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: targetFieldName },
|
||||
value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions
|
||||
name: {kind: Kind.NAME, value: targetFieldName},
|
||||
value: {kind: Kind.ENUM, value: sorter.direction}, // Adjust if your schema uses a different type for sorting directions
|
||||
};
|
||||
|
||||
// Add the new sorter condition
|
||||
@@ -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
|
||||
* @param ast
|
||||
* @param filters
|
||||
* @returns {ASTNode}
|
||||
*/
|
||||
export function applyFilters(ast, filters) {
|
||||
return visit(ast, {
|
||||
@@ -182,192 +272,197 @@ export function applyFilters(ast, filters) {
|
||||
filters.forEach(filter => {
|
||||
const fieldPath = filter.field.split('.');
|
||||
let topLevel = false;
|
||||
let topLevelSub = false;
|
||||
|
||||
// Determine if the filter should be applied at the top level
|
||||
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
|
||||
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
|
||||
if (fieldPath.length === 2) {
|
||||
topLevel = true;
|
||||
}
|
||||
|
||||
if (topLevel) {
|
||||
// Construct the filter for a top-level application
|
||||
const targetFieldName = fieldPath[fieldPath.length - 1];
|
||||
const filterValue = {
|
||||
kind: getGraphQLKind(filter.value),
|
||||
value: filter.value,
|
||||
};
|
||||
|
||||
const nestedFilter = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: targetFieldName },
|
||||
value: {
|
||||
kind: Kind.OBJECT,
|
||||
fields: [{
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: filter.operator },
|
||||
value: filterValue,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
// Find or create the where argument for the top-level field
|
||||
let whereArg = node.selectionSet.selections
|
||||
.find(selection => selection.name.value === fieldPath[0])
|
||||
?.arguments.find(arg => arg.name.value === 'where');
|
||||
|
||||
if (!whereArg) {
|
||||
whereArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: { kind: Kind.NAME, value: 'where' },
|
||||
value: { kind: Kind.OBJECT, fields: [] },
|
||||
};
|
||||
const topLevelSelection = node.selectionSet.selections.find(selection =>
|
||||
selection.name.value === fieldPath[0]
|
||||
);
|
||||
if (topLevelSelection) {
|
||||
topLevelSelection.arguments = topLevelSelection.arguments || [];
|
||||
topLevelSelection.arguments.push(whereArg);
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly position the nested filter without an extra 'where'
|
||||
if (fieldPath.length > 2) { // More than one level deep
|
||||
let currentField = whereArg.value;
|
||||
fieldPath.slice(1, -1).forEach((path, index) => {
|
||||
let existingField = currentField.fields.find(f => f.name.value === path);
|
||||
if (!existingField) {
|
||||
existingField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: path },
|
||||
value: { kind: Kind.OBJECT, fields: [] }
|
||||
};
|
||||
currentField.fields.push(existingField);
|
||||
}
|
||||
currentField = existingField.value;
|
||||
});
|
||||
currentField.fields.push(nestedFilter);
|
||||
} else { // Directly under the top level
|
||||
whereArg.value.fields.push(nestedFilter);
|
||||
}
|
||||
} else {
|
||||
// Initialize a reference to the current selection to traverse down the AST
|
||||
let currentSelection = node;
|
||||
let whereArgFound = false;
|
||||
|
||||
// Iterate over the fieldPath, except for the last entry, to navigate the structure
|
||||
for (let i = 0; i < fieldPath.length - 1; i++) {
|
||||
const fieldName = fieldPath[i];
|
||||
let fieldFound = false;
|
||||
|
||||
// Check if the current selection has a selectionSet and selections
|
||||
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
|
||||
// Look for the field in the current selection's selections
|
||||
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
|
||||
if (selection) {
|
||||
// Move down the AST to the found selection
|
||||
currentSelection = selection;
|
||||
fieldFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the field was not found in the current path, it's an issue
|
||||
if (!fieldFound) {
|
||||
console.error(`Field ${fieldName} not found in the current selection.`);
|
||||
return; // Exit the loop and function due to error
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, currentSelection should be the parent field where the filter needs to be applied
|
||||
// Check if the 'where' argument already exists in the current selection
|
||||
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
|
||||
if (whereArg) {
|
||||
whereArgFound = true;
|
||||
} else {
|
||||
// If not found, create a new 'where' argument for the current selection
|
||||
currentSelection.arguments.push({
|
||||
kind: Kind.ARGUMENT,
|
||||
name: { kind: Kind.NAME, value: 'where' },
|
||||
value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter
|
||||
});
|
||||
}
|
||||
|
||||
// Assuming the last entry in fieldPath is the field to apply the filter on
|
||||
const targetField = fieldPath[fieldPath.length - 1];
|
||||
const filterValue = {
|
||||
kind: getGraphQLKind(filter.value),
|
||||
value: filter.value,
|
||||
};
|
||||
|
||||
// Construct the filter field object
|
||||
const filterField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: targetField },
|
||||
value: {
|
||||
kind: Kind.OBJECT,
|
||||
fields: [{
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: filter.operator },
|
||||
value: filterValue,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
// Add the filter field to the 'where' clause of the current selection
|
||||
if (whereArgFound) {
|
||||
whereArg.value.fields.push(filterField);
|
||||
} else {
|
||||
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
|
||||
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
|
||||
}
|
||||
if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
|
||||
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
|
||||
topLevelSub = true;
|
||||
}
|
||||
|
||||
// Construct the filter for a top-level application
|
||||
const targetFieldName = fieldPath[fieldPath.length - 1];
|
||||
|
||||
let filterValue = createFilterValue(filter);
|
||||
let filterField = createFilterField(targetFieldName, filter, filterValue);
|
||||
|
||||
if (topLevel) {
|
||||
applyTopLevelFilter(node, fieldPath, filterField);
|
||||
} else if (topLevelSub) {
|
||||
applyTopLevelSub(node, fieldPath, filterField);
|
||||
} else {
|
||||
applyNestedFilter(node, fieldPath, filterField);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter value based on the filter
|
||||
* @param filter
|
||||
* @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
|
||||
*/
|
||||
function createFilterValue(filter) {
|
||||
if (Array.isArray(filter.value)) {
|
||||
// If it's an array, create a list value with the array items
|
||||
return {
|
||||
kind: Kind.LIST,
|
||||
values: filter.value.map(item => ({
|
||||
kind: getGraphQLKind(item),
|
||||
value: item,
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
// If it's not an array, use the existing logic
|
||||
return {
|
||||
kind: getGraphQLKind(filter.value),
|
||||
value: filter.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter field based on the target field and filter
|
||||
* @param targetFieldName
|
||||
* @param filter
|
||||
* @param filterValue
|
||||
* @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}}
|
||||
*/
|
||||
function createFilterField(targetFieldName, filter, filterValue) {
|
||||
return {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: targetFieldName},
|
||||
value: {
|
||||
kind: Kind.OBJECT,
|
||||
fields: [{
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: filter.operator},
|
||||
value: filterValue,
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a top-level filter to the AST
|
||||
* @param node
|
||||
* @param fieldPath
|
||||
* @param filterField
|
||||
*/
|
||||
function applyTopLevelFilter(node, fieldPath, filterField) {
|
||||
// Find or create the where argument for the top-level field
|
||||
let whereArg = node.selectionSet.selections
|
||||
.find(selection => selection.name.value === fieldPath[0])
|
||||
?.arguments.find(arg => arg.name.value === 'where');
|
||||
|
||||
if (!whereArg) {
|
||||
whereArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []},
|
||||
};
|
||||
const topLevelSelection = node.selectionSet.selections.find(selection =>
|
||||
selection.name.value === fieldPath[0]
|
||||
);
|
||||
if (topLevelSelection) {
|
||||
topLevelSelection.arguments = topLevelSelection.arguments || [];
|
||||
topLevelSelection.arguments.push(whereArg);
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly position the nested filter without an extra 'where'
|
||||
if (fieldPath.length > 2) { // More than one level deep
|
||||
let currentField = whereArg.value;
|
||||
fieldPath.slice(1, -1).forEach((path, index) => {
|
||||
let existingField = currentField.fields.find(f => f.name.value === path);
|
||||
if (!existingField) {
|
||||
existingField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: path},
|
||||
value: {kind: Kind.OBJECT, fields: []}
|
||||
};
|
||||
currentField.fields.push(existingField);
|
||||
}
|
||||
currentField = existingField.value;
|
||||
});
|
||||
currentField.fields.push(filterField);
|
||||
} else { // Directly under the top level
|
||||
whereArg.value.fields.push(filterField);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a nested filter to the AST
|
||||
* @param node
|
||||
* @param fieldPath
|
||||
* @param filterField
|
||||
*/
|
||||
function applyNestedFilter(node, fieldPath, filterField) {
|
||||
// Initialize a reference to the current selection to traverse down the AST
|
||||
let currentSelection = node;
|
||||
|
||||
// Iterate over the fieldPath, except for the last entry, to navigate the structure
|
||||
for (let i = 0; i < fieldPath.length - 1; i++) {
|
||||
const fieldName = fieldPath[i];
|
||||
let fieldFound = false;
|
||||
|
||||
// Check if the current selection has a selectionSet and selections
|
||||
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
|
||||
// Look for the field in the current selection's selections
|
||||
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
|
||||
if (selection) {
|
||||
// Move down the AST to the found selection
|
||||
currentSelection = selection;
|
||||
fieldFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the field was not found in the current path, it's an issue
|
||||
if (!fieldFound) {
|
||||
console.error(`Field ${fieldName} not found in the current selection.`);
|
||||
return; // Exit the loop and function due to error
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, currentSelection should be the parent field where the filter needs to be applied
|
||||
// Check if the 'where' argument already exists in the current selection
|
||||
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
|
||||
if (!whereArg) {
|
||||
// If not found, create a new 'where' argument for the current selection
|
||||
currentSelection.arguments.push({
|
||||
kind: Kind.ARGUMENT,
|
||||
name: {kind: Kind.NAME, value: 'where'},
|
||||
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
|
||||
});
|
||||
}
|
||||
|
||||
// Add the filter field to the 'where' clause of the current selection
|
||||
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GraphQL kind for a value
|
||||
* @param value
|
||||
* @returns {Kind|Kind.INT}
|
||||
*/
|
||||
function getGraphQLKind(value) {
|
||||
if (typeof value === 'number') {
|
||||
if (Array.isArray(value)) {
|
||||
return Kind.LIST;
|
||||
} else if (typeof value === 'number') {
|
||||
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return Kind.BOOLEAN;
|
||||
} else if (typeof value === 'string') {
|
||||
return Kind.STRING;
|
||||
} else if (value instanceof Date) {
|
||||
return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string
|
||||
}
|
||||
// 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 */
|
||||
|
||||
@@ -259,28 +259,30 @@
|
||||
- active:
|
||||
_eq: true
|
||||
columns:
|
||||
- id
|
||||
- billid
|
||||
- bodyshopid
|
||||
- created
|
||||
- operation
|
||||
- id
|
||||
- jobid
|
||||
- new_val
|
||||
- old_val
|
||||
- operation
|
||||
- type
|
||||
- useremail
|
||||
- bodyshopid
|
||||
- jobid
|
||||
- billid
|
||||
select_permissions:
|
||||
- role: user
|
||||
permission:
|
||||
columns:
|
||||
- billid
|
||||
- bodyshopid
|
||||
- created
|
||||
- id
|
||||
- jobid
|
||||
- new_val
|
||||
- old_val
|
||||
- operation
|
||||
- type
|
||||
- useremail
|
||||
- created
|
||||
- billid
|
||||
- bodyshopid
|
||||
- jobid
|
||||
filter:
|
||||
bodyshop:
|
||||
associations:
|
||||
@@ -4198,7 +4200,7 @@
|
||||
interval_sec: 10
|
||||
num_retries: 0
|
||||
timeout_sec: 60
|
||||
webhook_from_env: HASURA_API_URL
|
||||
webhook: https://worktest.home.irony.online
|
||||
headers:
|
||||
- name: event-secret
|
||||
value_from_env: EVENT_SECRET
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."audit_trail" add column "type" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."audit_trail" add column "type" text
|
||||
null;
|
||||
@@ -0,0 +1,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;
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."ioevents" add column "useremail" text;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."ioevents" add column "bodyshopid" uuid
|
||||
null;
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."ioevents" alter column "useremail" set not null;
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."ioevents" alter column "useremail" drop not null;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."ioevents" add column "env" text
|
||||
null;
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."ioevents_useremail";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "ioevents_useremail" on
|
||||
"public"."ioevents" using btree ("useremail");
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."ioevents" drop constraint "ioevents_useremail_fkey";
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."ioevents" drop constraint "ioevents_bodyshopid_fkey";
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."idx_audit_trail_type";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "idx_audit_trail_type" on
|
||||
"public"."audit_trail" using btree ("type");
|
||||
748
package-lock.json
generated
748
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@aws-sdk/client-secrets-manager": "^3.454.0",
|
||||
"@aws-sdk/client-ses": "^3.454.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.451.0",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@opensearch-project/opensearch": "^2.4.0",
|
||||
"aws4": "^1.12.0",
|
||||
"axios": "^1.6.2",
|
||||
@@ -52,6 +53,7 @@
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
},
|
||||
@@ -695,11 +697,496 @@
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"node_modules/@azure/abort-controller": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz",
|
||||
"integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-auth": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.6.0.tgz",
|
||||
"integrity": "sha512-3X9wzaaGgRaBCwhLQZDtFp5uLIXCPrGbwJNWPPugvL4xbIGgScv77YzzxToKGLAKvG9amDoofMoP+9hsH1vs1w==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-util": "^1.1.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-auth/node_modules/@azure/abort-controller": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
|
||||
"integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz",
|
||||
"integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-auth": "^1.3.0",
|
||||
"@azure/core-tracing": "1.0.0-preview.13",
|
||||
"@azure/core-util": "^1.1.1",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"@types/node-fetch": "^2.5.0",
|
||||
"@types/tunnel": "^0.0.3",
|
||||
"form-data": "^4.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"process": "^0.11.10",
|
||||
"tslib": "^2.2.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"uuid": "^8.3.0",
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/xml2js": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-lro": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.6.0.tgz",
|
||||
"integrity": "sha512-PyRNcaIOfMgoUC01/24NoG+k8O81VrKxYARnDlo+Q2xji0/0/j2nIt8BwQh294pb1c5QnXTDPbNR4KzoDKXEoQ==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-util": "^1.2.0",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-lro/node_modules/@azure/abort-controller": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
|
||||
"integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-paging": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz",
|
||||
"integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-tracing": {
|
||||
"version": "1.0.0-preview.13",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz",
|
||||
"integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.0.1",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-util": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.7.0.tgz",
|
||||
"integrity": "sha512-Zq2i3QO6k9DA8vnm29mYM4G8IE9u1mhF1GUabVEqPNX8Lj833gdxQ2NAFxt2BZsfAL+e9cT8SyVN7dFVJ/Hf0g==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-util/node_modules/@azure/abort-controller": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
|
||||
"integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/logger": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz",
|
||||
"integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/storage-blob": {
|
||||
"version": "12.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz",
|
||||
"integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^1.0.0",
|
||||
"@azure/core-http": "^3.0.0",
|
||||
"@azure/core-lro": "^2.2.0",
|
||||
"@azure/core-paging": "^1.1.1",
|
||||
"@azure/core-tracing": "1.0.0-preview.13",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"events": "^3.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
|
||||
"integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.23.4",
|
||||
"chalk": "^2.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame/node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.17.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
|
||||
"integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.17.0",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator/node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-environment-visitor": {
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
|
||||
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-function-name": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
|
||||
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/types": "^7.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-function-name/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
|
||||
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-split-export-declaration": {
|
||||
"version": "7.22.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
|
||||
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz",
|
||||
"integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==",
|
||||
"optional": true,
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
|
||||
"integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
|
||||
"integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"chalk": "^2.4.2",
|
||||
"js-tokens": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
|
||||
"integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
@@ -718,6 +1205,97 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
|
||||
"integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.23.5",
|
||||
"@babel/parser": "^7.23.9",
|
||||
"@babel/types": "^7.23.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
|
||||
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
"@babel/helper-function-name": "^7.23.0",
|
||||
"@babel/helper-hoist-variables": "^7.22.5",
|
||||
"@babel/helper-split-export-declaration": "^7.22.6",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/types": "^7.23.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse/node_modules/@babel/generator": {
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
|
||||
"integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.23.6",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"jsesc": "^2.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse/node_modules/@babel/types": {
|
||||
"version": "7.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
|
||||
"integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
||||
"integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
@@ -1034,6 +1612,54 @@
|
||||
"resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz",
|
||||
"integrity": "sha512-bIcKnH5YmtTYr7S6J3J86dn/rFiklwRpOqbTOQ9C0WMmR9FKHVb3bxs2UYfqEmNb93O4nbA97sb6rtz33i9SyA=="
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz",
|
||||
"integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.23",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz",
|
||||
"integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdoc/salty": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz",
|
||||
@@ -1106,6 +1732,14 @@
|
||||
"yarn": "^1.22.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz",
|
||||
"integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -1714,6 +2348,29 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@trivago/prettier-plugin-sort-imports": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz",
|
||||
"integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/generator": "7.17.7",
|
||||
"@babel/parser": "^7.20.5",
|
||||
"@babel/traverse": "7.23.2",
|
||||
"@babel/types": "7.17.0",
|
||||
"javascript-natural-sort": "0.7.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/compiler-sfc": "3.x",
|
||||
"prettier": "2.x - 3.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/compiler-sfc": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
@@ -1857,6 +2514,15 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
|
||||
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
|
||||
@@ -1906,6 +2572,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
|
||||
},
|
||||
"node_modules/@types/tunnel": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz",
|
||||
"integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
@@ -3307,6 +3981,14 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||
@@ -3854,6 +4536,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz",
|
||||
@@ -4446,6 +5137,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.15.4",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz",
|
||||
@@ -4459,6 +5156,12 @@
|
||||
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
|
||||
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
@@ -4540,6 +5243,18 @@
|
||||
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/json-2-csv": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.0.1.tgz",
|
||||
@@ -5457,6 +6172,14 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -6894,6 +7617,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -6949,6 +7681,14 @@
|
||||
"node": ">=0.6.x"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"engines": {
|
||||
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@aws-sdk/client-secrets-manager": "^3.454.0",
|
||||
"@aws-sdk/client-ses": "^3.454.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.451.0",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@opensearch-project/opensearch": "^2.4.0",
|
||||
"aws4": "^1.12.0",
|
||||
"axios": "^1.6.2",
|
||||
@@ -61,6 +62,7 @@
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
}
|
||||
|
||||
@@ -201,19 +201,24 @@ exports.default = async (req, res) => {
|
||||
} finally {
|
||||
sftp.end();
|
||||
}
|
||||
sendServerEmail({
|
||||
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
|
||||
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
|
||||
Uploaded: ${JSON.stringify(
|
||||
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
|
||||
null,
|
||||
2
|
||||
)}
|
||||
`,
|
||||
});
|
||||
// sendServerEmail({
|
||||
// subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
|
||||
// text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
|
||||
// Uploaded: ${JSON.stringify(
|
||||
// allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
|
||||
// null,
|
||||
// 2
|
||||
// )}
|
||||
// `,
|
||||
// });
|
||||
res.sendStatus(200);
|
||||
} catch (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))}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -11,27 +11,40 @@ require("dotenv").config({
|
||||
});
|
||||
|
||||
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 {
|
||||
// await client.request(queries.INSERT_IOEVENT, {
|
||||
// event: {
|
||||
// operationname: operationName,
|
||||
// time,
|
||||
// dbevent,
|
||||
// },
|
||||
// });
|
||||
console.log("IOEVENT", operationName, time, dbevent, user, imexshopid);
|
||||
logger.log("ioevent", "trace", user, null, {
|
||||
imexshopid,
|
||||
operationName,
|
||||
time,
|
||||
dbevent,
|
||||
await client.request(queries.INSERT_IOEVENT, {
|
||||
event: {
|
||||
operationname: operationName,
|
||||
time,
|
||||
dbevent,
|
||||
env,
|
||||
variables,
|
||||
bodyshopid,
|
||||
useremail,
|
||||
},
|
||||
});
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
res.status(400).send(error);
|
||||
logger.log("ioevent-error", "trace", user, null, {
|
||||
operationname: operationName,
|
||||
time,
|
||||
dbevent,
|
||||
env,
|
||||
variables,
|
||||
bodyshopid,
|
||||
useremail,
|
||||
});
|
||||
res.sendStatus(200);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ const queries = require("../graphql-client/queries");
|
||||
const moment = require("moment");
|
||||
const durationToHumanReadable = require("../utils/durationToHumanReadable");
|
||||
const calculateStatusDuration = require("../utils/calculateStatusDuration");
|
||||
const getLifecycleStatusColor = require("../utils/getLifecycleStatusColor");
|
||||
|
||||
const jobLifecycle = async (req, res) => {
|
||||
// Grab the jobids and statuses from the request body
|
||||
@@ -28,12 +29,12 @@ const jobLifecycle = async (req, res) => {
|
||||
jobIDs,
|
||||
transitions: []
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const transitionsByJobId = _.groupBy(resp.transitions, 'jobid');
|
||||
|
||||
const groupedTransitions = {};
|
||||
const allDurations = [];
|
||||
|
||||
for (let jobId in transitionsByJobId) {
|
||||
let lifecycle = transitionsByJobId[jobId].map(transition => {
|
||||
@@ -53,15 +54,57 @@ const jobLifecycle = async (req, res) => {
|
||||
return transition;
|
||||
});
|
||||
|
||||
const durations = calculateStatusDuration(lifecycle, statuses);
|
||||
|
||||
groupedTransitions[jobId] = {
|
||||
lifecycle: lifecycle,
|
||||
durations: calculateStatusDuration(lifecycle, statuses),
|
||||
lifecycle,
|
||||
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({
|
||||
jobIDs,
|
||||
transition: groupedTransitions,
|
||||
durations: {
|
||||
jobs: jobIDs.length,
|
||||
summations: finalSummations,
|
||||
totalStatuses: finalSummations.length,
|
||||
total: finalTotal,
|
||||
statusCounts: finalStatusCounts,
|
||||
humanReadable: durationToHumanReadable(moment.duration(finalTotal))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
const durationToHumanReadable = require("./durationToHumanReadable");
|
||||
const moment = require("moment");
|
||||
const getLifecycleStatusColor = require("./getLifecycleStatusColor");
|
||||
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) => {
|
||||
let statusDuration = {};
|
||||
@@ -33,26 +25,16 @@ const calculateStatusDuration = (transitions, statuses) => {
|
||||
if (!transition.prev_value) {
|
||||
statusDuration[transition.value] = {
|
||||
value: duration,
|
||||
humanReadable: transition.duration_readable
|
||||
humanReadable: durationToHumanReadable(moment.duration(duration))
|
||||
};
|
||||
} else if (!transition.next_value) {
|
||||
} else {
|
||||
if (statusDuration[transition.value]) {
|
||||
statusDuration[transition.value].value += duration;
|
||||
statusDuration[transition.value].humanReadable = transition.duration_readable;
|
||||
statusDuration[transition.value].humanReadable = durationToHumanReadable(moment.duration(statusDuration[transition.value].value));
|
||||
} else {
|
||||
statusDuration[transition.value] = {
|
||||
value: duration,
|
||||
humanReadable: transition.duration_readable
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (statusDuration[transition.value]) {
|
||||
statusDuration[transition.value].value += duration;
|
||||
statusDuration[transition.value].humanReadable = transition.duration_readable;
|
||||
} else {
|
||||
statusDuration[transition.value] = {
|
||||
value: duration,
|
||||
humanReadable: transition.duration_readable
|
||||
humanReadable: durationToHumanReadable(moment.duration(duration))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -79,7 +61,7 @@ const calculateStatusDuration = (transitions, statuses) => {
|
||||
value,
|
||||
humanReadable,
|
||||
percentage: statusDuration[status].percentage,
|
||||
color: getColor(status),
|
||||
color: getLifecycleStatusColor(status),
|
||||
roundedPercentage: `${Math.round(statusDuration[status].percentage)}%`
|
||||
});
|
||||
}
|
||||
|
||||
11
server/utils/getLifecycleStatusColor.js
Normal file
11
server/utils/getLifecycleStatusColor.js
Normal 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;
|
||||
Reference in New Issue
Block a user