Compare commits
81 Commits
rome/test-
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a45d0bb9f4 | ||
|
|
a2e0f9fbe7 | ||
|
|
e37dc0a18f | ||
|
|
c691d44c44 | ||
|
|
ebb3a13ff5 | ||
|
|
3846b7c5fc | ||
|
|
93d139f926 | ||
|
|
4d1f40537c | ||
|
|
896f1415f7 | ||
|
|
8a01cd9cb0 | ||
|
|
c2013d47e7 | ||
|
|
0f20807690 | ||
|
|
2a45be6a45 | ||
|
|
b63602143e | ||
|
|
2add712270 | ||
|
|
77c8f74bcb | ||
|
|
1d38102541 | ||
|
|
6117b5ab64 | ||
|
|
578f0a110e | ||
|
|
b5c66274ca | ||
|
|
be46bdc57f | ||
|
|
6263e63a1d | ||
|
|
83d702f12b | ||
|
|
37708a0b59 | ||
|
|
6cfcab8156 | ||
|
|
33ec18986d | ||
|
|
6b7b34ae79 | ||
|
|
06ef2482ba | ||
|
|
83bd485597 | ||
|
|
6921f2fe68 | ||
|
|
a7e199932c | ||
|
|
2427c14b7b | ||
|
|
66655d449e | ||
|
|
845a84c4c8 | ||
|
|
0ff5ea3d59 | ||
|
|
09492e647e | ||
|
|
2d6594cc73 | ||
|
|
3b8e83d88a | ||
|
|
3ec4dbb5b8 | ||
|
|
601fdbba39 | ||
|
|
830adc4ef3 | ||
|
|
3f216195ca | ||
|
|
bcb8de0937 | ||
|
|
9cc0d6175e | ||
|
|
4c07632ee6 | ||
|
|
767c219af8 | ||
|
|
cfc301570e | ||
|
|
fb718f9f37 | ||
|
|
cafc0e5628 | ||
|
|
a635725839 | ||
|
|
eb8e9b10ef | ||
|
|
2584f7129c | ||
|
|
6c996037d6 | ||
|
|
f421b45222 | ||
|
|
30cf46a158 | ||
|
|
8d37c54f89 | ||
|
|
f9b9f39418 | ||
|
|
05a5df789b | ||
|
|
bed87eda97 | ||
|
|
67008c35b8 | ||
|
|
9b95e40a13 | ||
|
|
e3a998b6f8 | ||
|
|
3c7ede0155 | ||
|
|
82c7aa1347 | ||
|
|
4ccd912363 | ||
|
|
c0525842fd | ||
|
|
dd5ca5d233 | ||
|
|
616a4b04a0 | ||
|
|
9a34640c88 | ||
|
|
3110be4703 | ||
|
|
971c8d41ff | ||
|
|
7c303a5154 | ||
|
|
9383b37a41 | ||
|
|
205d507097 | ||
|
|
97a1bd66d1 | ||
|
|
0d1ff6390c | ||
|
|
830d2c87d2 | ||
|
|
da98e7b886 | ||
|
|
a74a9ba5a1 | ||
|
|
b8a3081488 | ||
|
|
9b61da5c62 |
@@ -42,7 +42,7 @@ jobs:
|
||||
app-build:
|
||||
docker:
|
||||
- image: cimg/node:16.15.0
|
||||
|
||||
resource_class: large
|
||||
working_directory: ~/repo/client
|
||||
|
||||
steps:
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
test-app-build:
|
||||
docker:
|
||||
- image: cimg/node:16.15.0
|
||||
|
||||
resource_class: large
|
||||
working_directory: ~/repo/client
|
||||
|
||||
steps:
|
||||
@@ -159,4 +159,4 @@ workflows:
|
||||
#- admin-app-build:
|
||||
#filters:
|
||||
#branches:
|
||||
#only: master
|
||||
#only: master
|
||||
|
||||
167
_reference/reportFiltersAndSorters.md
Normal file
167
_reference/reportFiltersAndSorters.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Filters and Sorters
|
||||
|
||||
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
|
||||
modify the graphQL query and provide the user more power over their reports.
|
||||
|
||||
# Special Notes
|
||||
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
|
||||
|
||||
## High level Schema Overview
|
||||
|
||||
```javascript
|
||||
const schema = {
|
||||
"filters": [
|
||||
{
|
||||
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
|
||||
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
|
||||
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
|
||||
"type": "number" // Type of field, can be number or string currently
|
||||
},
|
||||
// ... more filters
|
||||
],
|
||||
"sorters": [
|
||||
{
|
||||
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
|
||||
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
|
||||
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
|
||||
"type": "number" // Type of field, can be number or string currently
|
||||
},
|
||||
// ... more sorters
|
||||
],
|
||||
"dates": {
|
||||
// This is not yet implemented and will be added in a future release
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Filters
|
||||
|
||||
Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
|
||||
A note on special notation used in the `name` field.
|
||||
|
||||
## Reflection
|
||||
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
|
||||
|
||||
```
|
||||
{
|
||||
"name": "jobs.status",
|
||||
"translation": "jobs.fields.status",
|
||||
"label": "Status",
|
||||
"type": "string",
|
||||
"reflector": {
|
||||
"type": "internal",
|
||||
"name": "special.job_statuses"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses`
|
||||
|
||||
The following cases are available
|
||||
|
||||
- `special.job_statuses` - This will reflect the statuses of the jobs table `bodyshop.md_ro_statuses.statuses'`
|
||||
- `special.cost_centers` - This will reflect the cost centers `bodyshop.md_responsibility_centers.costs`
|
||||
- `special.categories` - This will reflect the categories `bodyshop.md_categories`
|
||||
- `special.insurance_companies` - This will reflect the insurance companies `bodyshop.md_ins_cos`'
|
||||
- `special.employee_teams` - This will reflect the employee teams `bodyshop.employee_teams`
|
||||
- `special.employees` - This will reflect the employees `bodyshop.employees`
|
||||
- `special.first_names` - This will reflect the first names `bodyshop.employees`
|
||||
- `special.last_names` - This will reflect the last names `bodyshop.employees`
|
||||
-
|
||||
### Path without brackets, multi level
|
||||
|
||||
`"name": "jobs.joblines.mod_lb_hrs",`
|
||||
This will produce a where clause at the `joblines` level of the graphQL query,
|
||||
|
||||
```graphql
|
||||
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
|
||||
jobs(
|
||||
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}}
|
||||
) {
|
||||
joblines(
|
||||
order_by: {line_no: asc}
|
||||
where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}}
|
||||
) {
|
||||
line_no
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
convertedtolbr
|
||||
convertedtolbr_data
|
||||
}
|
||||
ownr_co_nm
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
plate_no
|
||||
ro_number
|
||||
status
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
v_model_yr
|
||||
v_vin
|
||||
v_color
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Path with brackets,top level
|
||||
`"name": "[jobs].joblines.mod_lb_hrs",`
|
||||
This will produce a where clause at the `jobs` level of the graphQL query.
|
||||
|
||||
```graphql
|
||||
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
|
||||
jobs(
|
||||
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}}
|
||||
) {
|
||||
joblines(
|
||||
order_by: {line_no: asc}
|
||||
where: {removed: {_eq: false}}
|
||||
) {
|
||||
line_no
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
convertedtolbr
|
||||
convertedtolbr_data
|
||||
}
|
||||
ownr_co_nm
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
plate_no
|
||||
ro_number
|
||||
status
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
v_model_yr
|
||||
v_vin
|
||||
v_color
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Known Caveats
|
||||
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
|
||||
- The `dates` object is not yet implemented and will be added in a future release.
|
||||
- The type object must be 'string' or 'number' and is case-sensitive.
|
||||
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
|
||||
- Do not add the ability to filter things that are already filtered as part of the original query, this would be redundant and could cause issues.
|
||||
- Do not add the ability to filter on things like FK constraints, must like the above example.
|
||||
|
||||
## Sorters
|
||||
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level.
|
||||
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters.
|
||||
|
||||
### Default Sorters
|
||||
- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
|
||||
- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "jobs.joblines.mod_lb_hrs",
|
||||
"translation": "jobs.joblines.mod_lb_hrs_1",
|
||||
"label": "mod_lb_hrs_1",
|
||||
"type": "number",
|
||||
"default": {
|
||||
"order": 1,
|
||||
"direction": "asc"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -3,10 +3,22 @@ import { useMutation } from "@apollo/client";
|
||||
import { Button, notification, Popconfirm } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { DELETE_BILL } from "../../graphql/bills.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
export default function BillDeleteButton({ bill, callback }) {
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
|
||||
|
||||
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [deleteBill] = useMutation(DELETE_BILL);
|
||||
@@ -36,6 +48,10 @@ export default function BillDeleteButton({ bill, callback }) {
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("bills.successes.deleted") });
|
||||
insertAuditTrail({
|
||||
jobid: jobid,
|
||||
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
|
||||
});
|
||||
|
||||
if (callback && typeof callback === "function") callback(bill.id);
|
||||
} else {
|
||||
|
||||
@@ -94,6 +94,7 @@ function BillEnterModalContainer({
|
||||
location,
|
||||
outstanding_returns,
|
||||
inventory,
|
||||
federal_tax_exempt,
|
||||
...remainingValues
|
||||
} = values;
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
@@ -58,7 +58,7 @@ export function BillsListTableComponent({
|
||||
<EditFilled />
|
||||
</Button>
|
||||
)}
|
||||
<BillDeleteButton bill={record} />
|
||||
<BillDeleteButton bill={record} jobid={job.id} />
|
||||
<BillDetailEditReturnComponent
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
||||
disabled={
|
||||
|
||||
@@ -68,6 +68,30 @@ export default function ContractFormComponent({
|
||||
<FormDateTimePicker />
|
||||
</Form.Item>
|
||||
)}
|
||||
{create && (
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
|
||||
>
|
||||
{() => {
|
||||
const insuranceOver =
|
||||
selectedCar &&
|
||||
selectedCar.insuranceexpires &&
|
||||
moment(selectedCar.insuranceexpires)
|
||||
.endOf("day")
|
||||
.isBefore(moment(form.getFieldValue("scheduledreturn")));
|
||||
if (insuranceOver)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.insuranceexpired")}
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
@@ -90,16 +114,17 @@ export default function ContractFormComponent({
|
||||
>
|
||||
{() => {
|
||||
const mileageOver =
|
||||
selectedCar &&
|
||||
selectedCar.nextservicekm <= form.getFieldValue("kmstart");
|
||||
|
||||
selectedCar && selectedCar.nextservicekm
|
||||
? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
|
||||
: false;
|
||||
const dueForService =
|
||||
selectedCar &&
|
||||
selectedCar.nextservicedate &&
|
||||
moment(selectedCar.nextservicedate).isBefore(
|
||||
moment(form.getFieldValue("scheduledreturn"))
|
||||
);
|
||||
|
||||
moment(selectedCar.nextservicedate)
|
||||
.endOf("day")
|
||||
.isSameOrBefore(
|
||||
moment(form.getFieldValue("scheduledreturn"))
|
||||
);
|
||||
if (mileageOver || dueForService)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
@@ -117,7 +142,6 @@ export default function ContractFormComponent({
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
@@ -72,7 +72,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
const { nextservicedate, nextservicekm, mileage } = record;
|
||||
const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
|
||||
record;
|
||||
|
||||
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
|
||||
|
||||
@@ -80,11 +81,25 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
nextservicedate &&
|
||||
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
|
||||
|
||||
const insuranceOver =
|
||||
insuranceexpires &&
|
||||
moment(insuranceexpires).endOf("day").isBefore(moment());
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{t(record.status)}
|
||||
{(mileageOver || dueForService) && (
|
||||
<Tooltip title={t("contracts.labels.cardueforservice")}>
|
||||
{(mileageOver || dueForService || insuranceOver) && (
|
||||
<Tooltip
|
||||
title={
|
||||
(mileageOver || dueForService) && insuranceOver
|
||||
? t("contracts.labels.insuranceexpired") +
|
||||
" / " +
|
||||
t("contracts.labels.cardueforservice")
|
||||
: insuranceOver
|
||||
? t("contracts.labels.insuranceexpired")
|
||||
: t("contracts.labels.cardueforservice")
|
||||
}
|
||||
>
|
||||
<WarningFilled style={{ color: "tomato" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
@@ -44,6 +45,13 @@ export default function CsiResponseFormContainer() {
|
||||
readOnly
|
||||
componentList={data.csi_by_pk.csiquestion.config}
|
||||
/>
|
||||
{data.csi_by_pk.validuntil ? (
|
||||
<>
|
||||
{t("csi.fields.validuntil")}
|
||||
{": "}
|
||||
<DateFormatter>{data.csi_by_pk.validuntil}</DateFormatter>
|
||||
</>
|
||||
) : null}
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -5,9 +5,11 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function CsiResponseListPaginated({
|
||||
refetch,
|
||||
@@ -16,23 +18,23 @@ export default function CsiResponseListPaginated({
|
||||
total,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { responseid, page, sortcolumn, sortorder } = search;
|
||||
const { responseid } = search;
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" },
|
||||
page: "",
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
width: "8%",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.ro_number || t("general.labels.na")}
|
||||
@@ -41,15 +43,18 @@ export default function CsiResponseListPaginated({
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||
width: "25%",
|
||||
sortOrder: sortcolumn === "owner" && sortorder,
|
||||
dataIndex: "owner_name",
|
||||
key: "owner_name",
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
OwnerNameDisplayFunction(a.job),
|
||||
OwnerNameDisplayFunction(b.job)
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.job.owner ? (
|
||||
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||
return record.job.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.job.ownerid}>
|
||||
<OwnerNameDisplay ownerObject={record.job} />
|
||||
</Link>
|
||||
) : (
|
||||
@@ -64,9 +69,9 @@ export default function CsiResponseListPaginated({
|
||||
dataIndex: "completedon",
|
||||
key: "completedon",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => a.completedon - b.completedon,
|
||||
width: "25%",
|
||||
sortOrder: sortcolumn === "completedon" && sortorder,
|
||||
sorter: (a, b) => dateSort(a.completedon, b.completedon),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.completedon ? (
|
||||
<DateFormatter>{record.completedon}</DateFormatter>
|
||||
@@ -76,11 +81,12 @@ export default function CsiResponseListPaginated({
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.columnKey;
|
||||
search.sortorder = sorter.order;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
setState({
|
||||
...state,
|
||||
filteredInfo: filters,
|
||||
sortedInfo: sorter,
|
||||
page: pagination.current,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
@@ -108,7 +114,7 @@ export default function CsiResponseListPaginated({
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: pageLimit,
|
||||
current: parseInt(page || 1),
|
||||
current: parseInt(state.page || 1),
|
||||
total: total,
|
||||
}}
|
||||
columns={columns}
|
||||
@@ -122,13 +128,6 @@ export default function CsiResponseListPaginated({
|
||||
selectedRowKeys: [responseid],
|
||||
type: "radio",
|
||||
}}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
handleOnRowClick(record);
|
||||
}, // click row
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -3,21 +3,32 @@ import {
|
||||
ExclamationCircleFilled,
|
||||
PauseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Space, Table, Tooltip } from "antd";
|
||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../../owner-name-display/owner-name-display.component";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
import {pageLimit} from "../../../utils/config";
|
||||
|
||||
export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {},
|
||||
});
|
||||
const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage(
|
||||
"isTvModeScheduledIn",
|
||||
false
|
||||
);
|
||||
|
||||
if (!data) return null;
|
||||
if (!data.scheduled_in_today)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
@@ -31,6 +42,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
alt_transport: item.job.alt_transport,
|
||||
clm_no: item.job.clm_no,
|
||||
jobid: item.job.jobid,
|
||||
joblines_body: item.job.joblines
|
||||
.filter((l) => l.mod_lbr_ty !== "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
|
||||
joblines_ref: item.job.joblines
|
||||
.filter((l) => l.mod_lbr_ty === "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
|
||||
ins_co_nm: item.job.ins_co_nm,
|
||||
iouparent: item.job.iouparent,
|
||||
ownerid: item.job.ownerid,
|
||||
@@ -49,7 +66,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
v_vin: item.job.v_vin,
|
||||
vehicleid: item.job.vehicleid,
|
||||
note: item.note,
|
||||
start: moment(item.start).format("hh:mm a"),
|
||||
start: item.start,
|
||||
title: item.title,
|
||||
};
|
||||
appt.push(i);
|
||||
@@ -59,11 +76,192 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
return new moment(a.start) - new moment(b.start);
|
||||
});
|
||||
|
||||
const columns = [
|
||||
const tvFontSize = 16;
|
||||
const tvFontWeight = "bold";
|
||||
|
||||
const tvColumns = [
|
||||
{
|
||||
title: t("appointments.fields.time"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.start, b.start),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<TimeFormatter>{record.start}</TimeFormatter>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Space>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{record.suspended && (
|
||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
||||
)}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
to={"/manage/owners/" + record.ownerid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
to={"/manage/vehicles/" + record.vehicleid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
|
||||
record.v_model_yr || ""
|
||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(appt &&
|
||||
appt
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.alt_transport}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lab"),
|
||||
dataIndex: "joblines_body",
|
||||
key: "joblines_body",
|
||||
sorter: (a, b) => a.joblines_body - b.joblines_body,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "joblines_body" &&
|
||||
state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.joblines_body.toFixed(1)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lar"),
|
||||
dataIndex: "joblines_ref",
|
||||
key: "joblines_ref",
|
||||
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.joblines_ref.toFixed(1)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("appointments.fields.time"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.start, b.start),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
|
||||
render: (text, record) => <TimeFormatter>{record.start}</TimeFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
@@ -91,7 +289,10 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
@@ -108,23 +309,16 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
title: t("dashboard.labels.phone"),
|
||||
dataIndex: "ownr_ph",
|
||||
key: "ownr_ph",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
<Space size="small" wrap>
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -134,7 +328,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
|
||||
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -142,6 +336,15 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
@@ -165,43 +368,80 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.time"),
|
||||
dataIndex: "start",
|
||||
key: "start",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
|
||||
filters:
|
||||
(appt &&
|
||||
appt
|
||||
.map((j) => j.ins_co_nm)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Ins. Co.*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.ins_co_nm),
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(appt &&
|
||||
appt
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (sorter) => {
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledintoday", {
|
||||
title={t("dashboard.titles.scheduledindate", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
extra={
|
||||
<Space>
|
||||
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
|
||||
<Switch
|
||||
onClick={() => setIsTvModeScheduledIn(!isTvModeScheduledIn)}
|
||||
defaultChecked={isTvModeScheduledIn}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
columns={isTvModeScheduledIn ? tvColumns : columns}
|
||||
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
||||
rowKey="id"
|
||||
style={{ height: "85%" }}
|
||||
dataSource={appt}
|
||||
size={isTvModeScheduledIn ? "small" : "middle"}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -220,6 +460,10 @@ export const DashboardScheduledInTodayGql = `
|
||||
alt_transport
|
||||
clm_no
|
||||
jobid: id
|
||||
joblines(where: {removed: {_eq: false}}) {
|
||||
mod_lb_hrs
|
||||
mod_lbr_ty
|
||||
}
|
||||
ins_co_nm
|
||||
iouparent
|
||||
ownerid
|
||||
|
||||
@@ -3,37 +3,272 @@ import {
|
||||
ExclamationCircleFilled,
|
||||
PauseCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Space, Table, Tooltip } from "antd";
|
||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../../owner-name-display/owner-name-display.component";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
import {pageLimit} from "../../../utils/config";
|
||||
|
||||
export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {},
|
||||
});
|
||||
const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage(
|
||||
"isTvModeScheduledOut",
|
||||
false
|
||||
);
|
||||
|
||||
if (!data) return null;
|
||||
if (!data.scheduled_out_today)
|
||||
return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
data.scheduled_out_today.forEach((item) => {
|
||||
item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
|
||||
item.joblines_body = item.joblines
|
||||
? item.joblines
|
||||
.filter((l) => l.mod_lbr_ty !== "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||
: 0;
|
||||
item.joblines_ref = item.joblines
|
||||
? item.joblines
|
||||
.filter((l) => l.mod_lbr_ty === "LAR")
|
||||
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||
: 0;
|
||||
});
|
||||
data.scheduled_out_today.sort(function (a, b) {
|
||||
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
|
||||
});
|
||||
|
||||
const columns = [
|
||||
const tvFontSize = 18;
|
||||
const tvFontWeight = "bold";
|
||||
|
||||
const tvColumns = [
|
||||
{
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
dateSort(a.scheduled_completion, b.scheduled_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "scheduled_completion" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Space>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{record.suspended && (
|
||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
||||
)}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
to={"/manage/owners/" + record.ownerid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
to={"/manage/vehicles/" + record.vehicleid}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
|
||||
record.v_model_yr || ""
|
||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(data.scheduled_out_today &&
|
||||
data.scheduled_out_today
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.alt_transport}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filters:
|
||||
(data.scheduled_out_today &&
|
||||
data.scheduled_out_today
|
||||
.map((j) => j.status)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Status*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.status}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lab"),
|
||||
dataIndex: "joblines_body",
|
||||
key: "joblines_body",
|
||||
sorter: (a, b) => a.joblines_body - b.joblines_body,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "joblines_body" &&
|
||||
state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.joblines_body.toFixed(1)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lar"),
|
||||
dataIndex: "joblines_ref",
|
||||
key: "joblines_ref",
|
||||
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.joblines_ref.toFixed(1)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
dateSort(a.scheduled_completion, b.scheduled_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "scheduled_completion" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link
|
||||
to={"/manage/jobs/" + record.jobid}
|
||||
@@ -61,7 +296,10 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link
|
||||
@@ -78,23 +316,16 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
title: t("dashboard.labels.phone"),
|
||||
dataIndex: "ownr_ph",
|
||||
key: "ownr_ph",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
<Space size="small" wrap>
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -104,7 +335,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} />
|
||||
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -112,6 +343,15 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link
|
||||
@@ -135,43 +375,80 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
|
||||
filters:
|
||||
(data.scheduled_out_today &&
|
||||
data.scheduled_out_today
|
||||
.map((j) => j.ins_co_nm)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Ins. Co.*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.ins_co_nm),
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "alt_transport" &&
|
||||
state.sortedInfo.order,
|
||||
filters:
|
||||
(data.scheduled_out_today &&
|
||||
data.scheduled_out_today
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (sorter) => {
|
||||
setState({ ...state, sortedInfo: sorter });
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduledouttoday", {
|
||||
title={t("dashboard.titles.scheduledoutdate", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
})}
|
||||
extra={
|
||||
<Space>
|
||||
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
|
||||
<Switch
|
||||
onClick={() => setIsTvModeScheduledOut(!isTvModeScheduledOut)}
|
||||
defaultChecked={isTvModeScheduledOut}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
columns={isTvModeScheduledOut ? tvColumns : columns}
|
||||
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
||||
rowKey="id"
|
||||
style={{ height: "85%" }}
|
||||
dataSource={data.scheduled_out_today}
|
||||
size={isTvModeScheduledOut ? "small" : "middle"}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -188,6 +465,10 @@ export const DashboardScheduledOutTodayGql = `
|
||||
alt_transport
|
||||
clm_no
|
||||
jobid: id
|
||||
joblines(where: {removed: {_eq: false}}) {
|
||||
mod_lb_hrs
|
||||
mod_lbr_ty
|
||||
}
|
||||
ins_co_nm
|
||||
iouparent
|
||||
ownerid
|
||||
@@ -200,6 +481,7 @@ export const DashboardScheduledOutTodayGql = `
|
||||
production_vars
|
||||
ro_number
|
||||
scheduled_completion
|
||||
status
|
||||
suspended
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
|
||||
@@ -275,26 +275,22 @@ const componentList = {
|
||||
h: 2,
|
||||
},
|
||||
ScheduleInToday: {
|
||||
label: i18next.t("dashboard.titles.scheduledintoday", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
}),
|
||||
label: i18next.t("dashboard.titles.scheduledintoday"),
|
||||
component: DashboardScheduledInToday,
|
||||
gqlFragment: DashboardScheduledInTodayGql,
|
||||
minW: 10,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
w: 10,
|
||||
h: 2,
|
||||
h: 3,
|
||||
},
|
||||
ScheduleOutToday: {
|
||||
label: i18next.t("dashboard.titles.scheduledouttoday", {
|
||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
||||
}),
|
||||
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
||||
component: DashboardScheduledOutToday,
|
||||
gqlFragment: DashboardScheduledOutTodayGql,
|
||||
minW: 10,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
w: 10,
|
||||
h: 2,
|
||||
h: 3,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -306,8 +302,7 @@ const createDashboardQuery = (state) => {
|
||||
.map((item, index) => componentList[item.i].gqlFragment || "")
|
||||
.join("");
|
||||
return gql`
|
||||
query QUERY_DASHBOARD_DETAILS {
|
||||
${componentBasedAdditions || ""}
|
||||
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
||||
monthly_sales: jobs(where: {_and: [
|
||||
{ voided: {_eq: false}},
|
||||
{date_invoiced: {_gte: "${moment()
|
||||
@@ -317,11 +312,11 @@ const createDashboardQuery = (state) => {
|
||||
.endOf("month")
|
||||
.endOf("day")
|
||||
.toISOString()}"}}]}) {
|
||||
id
|
||||
ro_number
|
||||
date_invoiced
|
||||
job_totals
|
||||
rate_la1
|
||||
id
|
||||
ro_number
|
||||
date_invoiced
|
||||
job_totals
|
||||
rate_la1
|
||||
rate_la2
|
||||
rate_la3
|
||||
rate_la4
|
||||
@@ -344,43 +339,42 @@ const createDashboardQuery = (state) => {
|
||||
rate_mapa
|
||||
rate_mash
|
||||
rate_matd
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
}
|
||||
}
|
||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
ro_number
|
||||
ins_co_nm
|
||||
job_totals
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
ro_number
|
||||
ins_co_nm
|
||||
job_totals
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
act_price
|
||||
part_qty
|
||||
part_type
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}`;
|
||||
};
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.ant-card-body {
|
||||
height: 80%;
|
||||
height: calc(100% - 2rem);
|
||||
width: 100%;
|
||||
// // background-color: red;
|
||||
// height: 90%;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useMutation, useLazyQuery } from "@apollo/client";
|
||||
import { CheckCircleOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
InputNumber,
|
||||
notification,
|
||||
Popover,
|
||||
Space,
|
||||
notification,
|
||||
} from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
@@ -50,6 +50,7 @@ export default function ScoreboardAddButton({
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("job_close_add_to_scoreboard");
|
||||
values.date = moment(values.date).format("YYYY-MM-DD");
|
||||
|
||||
setLoading(true);
|
||||
let result;
|
||||
@@ -177,7 +178,7 @@ export default function ScoreboardAddButton({
|
||||
return acc + job.lbr_adjustments[val];
|
||||
}, 0);
|
||||
form.setFieldsValue({
|
||||
date: new moment(),
|
||||
date: moment(),
|
||||
bodyhrs: Math.round(v.bodyhrs * 10) / 10,
|
||||
painthrs: Math.round(v.painthrs * 10) / 10,
|
||||
});
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
useQuery,
|
||||
} from "@apollo/client";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Col, notification, Row } from "antd";
|
||||
import { Col, Row, notification } from "antd";
|
||||
import Axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
import moment from "moment";
|
||||
import moment from "moment-business-days";
|
||||
import queryString from "query-string";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import confirmDialog from "../../utils/asyncConfirm";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import confirmDialog from "../../utils/asyncConfirm";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
||||
@@ -73,7 +73,15 @@ export function JobsAvailableContainer({
|
||||
|
||||
const [selectedJob, setSelectedJob] = useState(null);
|
||||
const [selectedOwner, setSelectedOwner] = useState(null);
|
||||
const [partsQueueToggle, setPartsQueueToggle] = useState(bodyshop.md_functionality_toggles.parts_queue_toggle);
|
||||
const [partsQueueToggle, setPartsQueueToggle] = useState(
|
||||
bodyshop.md_functionality_toggles.parts_queue_toggle
|
||||
);
|
||||
const [updateSchComp, setSchComp] = useState({
|
||||
actual_in: moment(),
|
||||
checked: false,
|
||||
scheduled_completion: moment(),
|
||||
automatic: false,
|
||||
});
|
||||
|
||||
const [insertLoading, setInsertLoading] = useState(false);
|
||||
|
||||
@@ -197,11 +205,16 @@ export function JobsAvailableContainer({
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.creating", { error: err.message }),
|
||||
});
|
||||
refetch().catch(e => {console.error(`Something went wrong in jobs available table container - ${err.message || ''}`)});
|
||||
refetch().catch((e) => {
|
||||
console.error(
|
||||
`Something went wrong in jobs available table container - ${
|
||||
err.message || ""
|
||||
}`
|
||||
);
|
||||
});
|
||||
setInsertLoading(false);
|
||||
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
//Supplement scenario
|
||||
@@ -225,6 +238,22 @@ export function JobsAvailableContainer({
|
||||
//IO-539 Check for Parts Rate on PAL for SGI use case.
|
||||
await CheckTaxRates(supp, bodyshop);
|
||||
|
||||
if (updateSchComp.checked === true) {
|
||||
if (updateSchComp.automatic === true) {
|
||||
const job_hrs = supp.joblines.data.reduce(
|
||||
(acc, val) => acc + val.mod_lb_hrs,
|
||||
0
|
||||
);
|
||||
const num_days = job_hrs / bodyshop.target_touchtime;
|
||||
supp.actual_in = updateSchComp.actual_in;
|
||||
supp.scheduled_completion = moment(
|
||||
updateSchComp.actual_in
|
||||
).businessAdd(num_days, "days");
|
||||
} else {
|
||||
supp.scheduled_completion = updateSchComp.scheduled_completion;
|
||||
}
|
||||
}
|
||||
|
||||
delete supp.owner;
|
||||
delete supp.vehicle;
|
||||
delete supp.ins_co_nm;
|
||||
@@ -261,9 +290,9 @@ export function JobsAvailableContainer({
|
||||
},
|
||||
});
|
||||
|
||||
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
|
||||
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
|
||||
|
||||
if (CriticalPartsScanning.treatment === "on") {
|
||||
if (CriticalPartsScanning.treatment === "on") {
|
||||
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id);
|
||||
}
|
||||
if (updateResult.errors) {
|
||||
@@ -367,7 +396,6 @@ export function JobsAvailableContainer({
|
||||
|
||||
if (error) return <AlertComponent type="error" message={error.message} />;
|
||||
|
||||
|
||||
return (
|
||||
<LoadingSpinner
|
||||
loading={insertLoading}
|
||||
@@ -384,7 +412,6 @@ export function JobsAvailableContainer({
|
||||
visible={ownerModalVisible}
|
||||
onOk={onOwnerFindModalOk}
|
||||
onCancel={onOwnerModalCancel}
|
||||
|
||||
/>
|
||||
<JobsFindModalContainer
|
||||
loading={estDataRaw.loading}
|
||||
@@ -398,6 +425,8 @@ export function JobsAvailableContainer({
|
||||
modalSearchState={modalSearchState}
|
||||
partsQueueToggle={partsQueueToggle}
|
||||
setPartsQueueToggle={setPartsQueueToggle}
|
||||
updateSchComp={updateSchComp}
|
||||
setSchComp={setSchComp}
|
||||
/>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
|
||||
@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
function updateJobCache(items) {
|
||||
client.cache.modify({
|
||||
id: "ROOT_QUERY",
|
||||
@@ -40,6 +47,7 @@ export function JobsCloseExportButton({
|
||||
disabled,
|
||||
setSelectedJobs,
|
||||
refetch,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
@@ -181,6 +189,10 @@ export function JobsCloseExportButton({
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
|
||||
);
|
||||
@@ -192,12 +204,20 @@ export function JobsCloseExportButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
|
||||
if (
|
||||
bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
successfulTransactions.length > 0
|
||||
) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
});
|
||||
updateJobCache([
|
||||
...new Set(
|
||||
successfulTransactions.map(
|
||||
@@ -227,4 +247,7 @@ export function JobsCloseExportButton({
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(JobsCloseExportButton);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsCloseExportButton);
|
||||
|
||||
@@ -129,6 +129,12 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
},
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobsuspend(
|
||||
!!job.suspended ? !job.suspended : true
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const statusmenu = (
|
||||
@@ -540,6 +546,10 @@ export function JobsDetailHeaderActions({
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.voided"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobvoid(),
|
||||
});
|
||||
//go back to jobs list.
|
||||
history.push(`/manage/`);
|
||||
} else {
|
||||
|
||||
@@ -123,20 +123,23 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
</DataLabel>
|
||||
{job.cccontracts.length > 0 && (
|
||||
<DataLabel label={t("jobs.labels.contracts")}>
|
||||
{job.cccontracts.map((c) => (
|
||||
<Link
|
||||
key={c.id}
|
||||
to={`/manage/courtesycars/contracts/${c.id}`}
|
||||
>{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}</Link>
|
||||
{job.cccontracts.map((c, index) => (
|
||||
<Space wrap>
|
||||
<Link
|
||||
key={c.id}
|
||||
to={`/manage/courtesycars/contracts/${c.id}`}
|
||||
>
|
||||
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
||||
{index !== job.cccontracts.length - 1 ? "," : null}
|
||||
</Link>
|
||||
</Space>
|
||||
))}
|
||||
</DataLabel>
|
||||
)}
|
||||
|
||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||
<ProductionListColumnProductionNote record={job} />
|
||||
</DataLabel>
|
||||
|
||||
<Space>
|
||||
<Space wrap>
|
||||
{job.special_coverage_policy && (
|
||||
<Tag color="tomato">
|
||||
<Space>
|
||||
|
||||
@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_JOBS } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
function updateJobCache(items) {
|
||||
client.cache.modify({
|
||||
id: "ROOT_QUERY",
|
||||
@@ -41,6 +48,7 @@ export function JobsExportAllButton({
|
||||
loadingCallback,
|
||||
completedCallback,
|
||||
refetch,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [updateJob] = useMutation(UPDATE_JOBS);
|
||||
@@ -177,6 +185,12 @@ export function JobsExportAllButton({
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
});
|
||||
});
|
||||
updateJobCache(
|
||||
jobUpdateResponse.data.update_jobs.returning.map(
|
||||
(job) => job.id
|
||||
@@ -190,13 +204,17 @@ export function JobsExportAllButton({
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
|
||||
if (
|
||||
bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
successfulTransactions.length > 0
|
||||
) {
|
||||
notification.open({
|
||||
type: "success",
|
||||
key: "jobsuccessexport",
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
updateJobCache([
|
||||
const successfulTransactionsSet = [
|
||||
...new Set(
|
||||
successfulTransactions.map(
|
||||
(st) =>
|
||||
@@ -207,7 +225,14 @@ export function JobsExportAllButton({
|
||||
]
|
||||
)
|
||||
),
|
||||
]);
|
||||
];
|
||||
if (successfulTransactionsSet.length > 0) {
|
||||
insertAuditTrail({
|
||||
jobid: successfulTransactionsSet[0],
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
});
|
||||
}
|
||||
updateJobCache(successfulTransactionsSet);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -225,4 +250,7 @@ export function JobsExportAllButton({
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(JobsExportAllButton);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsExportAllButton);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Checkbox, Divider, Input, Table, Button } from "antd";
|
||||
import React from "react";
|
||||
import { Button, Checkbox, Divider, Input, Space, Table } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
|
||||
export default function JobsFindModalComponent({
|
||||
@@ -16,11 +18,13 @@ export default function JobsFindModalComponent({
|
||||
jobsListRefetch,
|
||||
partsQueueToggle,
|
||||
setPartsQueueToggle,
|
||||
updateSchComp,
|
||||
setSchComp,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [modalSearch, setModalSearch] = modalSearchState;
|
||||
const [importOptions, setImportOptions] = importOptionsState;
|
||||
|
||||
const [checkUTT, setCheckUTT] = useState(false);
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
@@ -142,6 +146,35 @@ export default function JobsFindModalComponent({
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
setSelectedJob(record.id);
|
||||
if (record.actual_in && record.scheduled_completion) {
|
||||
setSchComp({
|
||||
...updateSchComp,
|
||||
actual_in: record.actual_in,
|
||||
scheduled_completion: record.scheduled_completion,
|
||||
});
|
||||
} else {
|
||||
if (record.actual_in && !record.scheduled_completion) {
|
||||
setSchComp({
|
||||
...updateSchComp,
|
||||
actual_in: record.actual_in,
|
||||
scheduled_completion: moment(),
|
||||
});
|
||||
}
|
||||
if (!record.actual_in && record.scheduled_completion) {
|
||||
setSchComp({
|
||||
...updateSchComp,
|
||||
actual_in: moment(),
|
||||
scheduled_completion: moment(record.scheduled_completion),
|
||||
});
|
||||
}
|
||||
if (!record.actual_in && !record.scheduled_completion) {
|
||||
setSchComp({
|
||||
...updateSchComp,
|
||||
actual_in: moment(),
|
||||
scheduled_completion: moment(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -177,6 +210,35 @@ export default function JobsFindModalComponent({
|
||||
rowSelection={{
|
||||
onSelect: (props) => {
|
||||
setSelectedJob(props.id);
|
||||
if (props.actual_in && props.scheduled_completion) {
|
||||
setSchComp({
|
||||
...updateSchComp,
|
||||
actual_in: props.actual_in,
|
||||
scheduled_completion: props.scheduled_completion,
|
||||
});
|
||||
} else {
|
||||
if (props.actual_in && !props.scheduled_completion) {
|
||||
setSchComp({
|
||||
...updateSchComp,
|
||||
actual_in: props.actual_in,
|
||||
scheduled_completion: moment(),
|
||||
});
|
||||
}
|
||||
if (!props.actual_in && props.scheduled_completion) {
|
||||
setSchComp({
|
||||
...updateSchComp,
|
||||
actual_in: moment(),
|
||||
scheduled_completion: moment(props.scheduled_completion),
|
||||
});
|
||||
}
|
||||
if (!props.actual_in && !props.scheduled_completion) {
|
||||
setSchComp({
|
||||
...updateSchComp,
|
||||
actual_in: moment(),
|
||||
scheduled_completion: moment(),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedJob],
|
||||
@@ -190,23 +252,58 @@ export default function JobsFindModalComponent({
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
<Checkbox
|
||||
defaultChecked={importOptions.overrideHeader}
|
||||
onChange={(e) =>
|
||||
setImportOptions({
|
||||
...importOptions,
|
||||
overrideHeaders: e.target.checked,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("jobs.labels.override_header")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
<Space>
|
||||
<Checkbox
|
||||
defaultChecked={importOptions.overrideHeader}
|
||||
onChange={(e) =>
|
||||
setImportOptions({
|
||||
...importOptions,
|
||||
overrideHeaders: e.target.checked,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("jobs.labels.override_header")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={partsQueueToggle}
|
||||
onChange={(e) => setPartsQueueToggle(e.target.checked)}
|
||||
>
|
||||
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
||||
</Checkbox>
|
||||
>
|
||||
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={updateSchComp.checked}
|
||||
onChange={(e) =>
|
||||
setSchComp({ ...updateSchComp, checked: e.target.checked })
|
||||
}
|
||||
>
|
||||
{t("jobs.labels.update_scheduled_completion")}
|
||||
</Checkbox>
|
||||
{updateSchComp.checked === true ? (
|
||||
<>
|
||||
{checkUTT === false ? (
|
||||
<FormDateTimePickerComponent
|
||||
value={updateSchComp.scheduled_completion}
|
||||
onChange={(e) => {
|
||||
setSchComp({ ...updateSchComp, scheduled_completion: e });
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Checkbox
|
||||
checked={checkUTT}
|
||||
onChange={(e) => {
|
||||
setCheckUTT(e.target.checked);
|
||||
setSchComp({
|
||||
...updateSchComp,
|
||||
scheduled_completion: null,
|
||||
automatic: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("jobs.labels.calc_scheuled_completion")}
|
||||
</Checkbox>
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ export default connect(
|
||||
modalSearchState,
|
||||
partsQueueToggle,
|
||||
setPartsQueueToggle,
|
||||
updateSchComp,
|
||||
setSchComp,
|
||||
...modalProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -95,6 +97,8 @@ export default connect(
|
||||
modalSearchState={modalSearchState}
|
||||
partsQueueToggle={partsQueueToggle}
|
||||
setPartsQueueToggle={setPartsQueueToggle}
|
||||
updateSchComp={updateSchComp}
|
||||
setSchComp={setSchComp}
|
||||
/>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Card, Divider, Drawer, Grid } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { QUERY_PARTS_QUEUE_CARD_DETAILS } from "../../graphql/jobs.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import PartsQueueJobLinesComponent from "./parts-queue-job-lines.component";
|
||||
|
||||
export default function PartsQueueDetailCard() {
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
|
||||
const bpoints = {
|
||||
xs: "100%",
|
||||
sm: "100%",
|
||||
md: "100%",
|
||||
lg: "75%",
|
||||
xl: "75%",
|
||||
xxl: "60%",
|
||||
};
|
||||
const drawerPercentage = selectedBreakpoint
|
||||
? bpoints[selectedBreakpoint[0]]
|
||||
: "100%";
|
||||
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { selected } = searchParams;
|
||||
const history = useHistory();
|
||||
const { loading, error, data } = useQuery(QUERY_PARTS_QUEUE_CARD_DETAILS, {
|
||||
variables: { id: selected },
|
||||
skip: !selected,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleDrawerClose = () => {
|
||||
delete searchParams.selected;
|
||||
history.push({
|
||||
search: queryString.stringify({
|
||||
...searchParams,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
visible={!!selected}
|
||||
destroyOnClose
|
||||
width={drawerPercentage}
|
||||
placement="right"
|
||||
onClose={handleDrawerClose}
|
||||
>
|
||||
{loading ? <LoadingSpinner /> : null}
|
||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||
{data ? (
|
||||
<Card
|
||||
title={
|
||||
<Link to={`/manage/jobs/${data.jobs_by_pk.id}`}>
|
||||
{data.jobs_by_pk.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<JobsDetailHeader job={data ? data.jobs_by_pk : null} />
|
||||
<Divider type="horizontal" />
|
||||
<PartsQueueJobLinesComponent
|
||||
jobLines={data.jobs_by_pk ? data.jobs_by_pk.joblines : null}
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { Card, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function PartsQueueJobLinesComponent({ jobRO, loading, jobLines }) {
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {},
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "#",
|
||||
dataIndex: "line_no",
|
||||
key: "line_no",
|
||||
sorter: (a, b) => a.line_no - b.line_no,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
onCell: (record) => ({
|
||||
className: record.manual_line && "job-line-manual",
|
||||
style: {
|
||||
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
|
||||
},
|
||||
}),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
render: (text, record) =>
|
||||
`${record.oem_partno || ""} ${
|
||||
record.alt_partno ? `(${record.alt_partno})` : ""
|
||||
}`.trim(),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
filteredValue: state.filteredInfo.part_type || null,
|
||||
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
|
||||
filters: [
|
||||
{
|
||||
text: t("jobs.labels.partsfilter"),
|
||||
value: [
|
||||
"PAN",
|
||||
"PAC",
|
||||
"PAR",
|
||||
"PAL",
|
||||
"PAA",
|
||||
"PAM",
|
||||
"PAP",
|
||||
"PAS",
|
||||
"PASL",
|
||||
"PAG",
|
||||
],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAN"),
|
||||
value: ["PAN"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAP"),
|
||||
value: ["PAP"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAL"),
|
||||
value: ["PAL"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAA"),
|
||||
value: ["PAA"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAG"),
|
||||
value: ["PAG"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAS"),
|
||||
value: ["PAS"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PASL"),
|
||||
value: ["PASL"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAC"),
|
||||
value: ["PAC"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAR"),
|
||||
value: ["PAR"],
|
||||
},
|
||||
{
|
||||
text: t("joblines.fields.part_types.PAM"),
|
||||
value: ["PAM"],
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => value.includes(record.part_type),
|
||||
render: (text, record) =>
|
||||
record.part_type
|
||||
? t(`joblines.fields.part_types.${record.part_type}`)
|
||||
: null,
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.part_qty"),
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty",
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>
|
||||
{record.db_ref === "900510" || record.db_ref === "900511"
|
||||
? record.prt_dsmk_m
|
||||
: record.act_price}
|
||||
</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.location"),
|
||||
dataIndex: "location",
|
||||
key: "location",
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filteredValue: state.filteredInfo.status || null,
|
||||
filters:
|
||||
(jobLines &&
|
||||
jobLines
|
||||
.map((l) => l.status)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Status*",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
filteredInfo: filters,
|
||||
sortedInfo: sorter,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("jobs.labels.parts_lines")}>
|
||||
<Table
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
dataSource={jobLines}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(PartsQueueJobLinesComponent);
|
||||
@@ -8,30 +8,28 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component";
|
||||
import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component";
|
||||
import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component";
|
||||
import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component";
|
||||
import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function PartsQueuePageComponent({ bodyshop }) {
|
||||
export function PartsQueueListComponent({ bodyshop }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const {
|
||||
//page,
|
||||
sortcolumn,
|
||||
sortorder,
|
||||
statusFilters,
|
||||
} = searchParams;
|
||||
const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
|
||||
const history = useHistory();
|
||||
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
|
||||
|
||||
@@ -39,19 +37,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
// offset: page ? (page - 1) * 25 : 0,
|
||||
// limit: 25,
|
||||
statuses: (statusFilters && JSON.parse(statusFilters)) ||
|
||||
bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "ro_number"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -107,6 +94,19 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
history.push({ search: queryString.stringify(searchParams) });
|
||||
};
|
||||
|
||||
const handleOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
history.push({
|
||||
search: queryString.stringify({
|
||||
...searchParams,
|
||||
selected: record.id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
@@ -125,7 +125,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "ownr_ln",
|
||||
key: "ownr_ln",
|
||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
@@ -139,6 +140,56 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
|
||||
a.v_model_desc || ""
|
||||
}`,
|
||||
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
|
||||
),
|
||||
sortOrder: sortcolumn === "vehicle" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm_short"),
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder: sortcolumn === "ins_co_nm" && sortorder,
|
||||
filteredValue: filter?.ins_co_nm || null,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
.map((j) => j.ins_co_nm)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Ins. Co.*",
|
||||
value: [s],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.ins_co_nm),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
@@ -170,23 +221,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
title: t("jobs.fields.scheduled_completion"),
|
||||
dataIndex: "scheduled_completion",
|
||||
key: "scheduled_completion",
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||
record.v_model_desc || ""
|
||||
}`}</span>
|
||||
);
|
||||
},
|
||||
sorter: (a, b) =>
|
||||
dateSort(a.scheduled_completion, b.scheduled_completion),
|
||||
sortOrder: sortcolumn === "scheduled_completion" && sortorder,
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.scheduled_completion}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// title: t("vehicles.fields.plate_no"),
|
||||
@@ -198,14 +242,6 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
// return record.plate_no ? record.plate_no : "";
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: t("jobs.fields.clm_no"),
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||
},
|
||||
// {
|
||||
// title: t("jobs.fields.clm_total"),
|
||||
// dataIndex: "clm_total",
|
||||
@@ -307,9 +343,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
style={{ height: "100%" }}
|
||||
scroll={{ x: true }}
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record) => {
|
||||
handleOnRowClick(record);
|
||||
},
|
||||
selectedRowKeys: [selected],
|
||||
type: "radio",
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(PartsQueuePageComponent);
|
||||
export default connect(mapStateToProps, null)(PartsQueueListComponent);
|
||||
@@ -0,0 +1,369 @@
|
||||
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd";
|
||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import {fetchFilterData} from "../../utils/RenderTemplate";
|
||||
import {DeleteFilled} from "@ant-design/icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import {generateInternalReflections} from "./report-center-modal-utils";
|
||||
|
||||
|
||||
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
|
||||
return (
|
||||
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
return <RenderFilters form={form} templateId={key} bodyshop={bodyshop}/>;
|
||||
}}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters Section
|
||||
* @param filters
|
||||
* @param form
|
||||
* @param bodyshop
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function FiltersSection({filters, form, bodyshop}) {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
|
||||
<Form.List name={["filters"]}>
|
||||
{(fields, {add, remove, move}) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
key={`${index}field`}
|
||||
label={t('reportcenter.labels.advanced_filters_filter_field')}
|
||||
name={[field.name, "field"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
onChange={() => {
|
||||
// Clear related Fields
|
||||
form.setFieldValue(['filters', field.name, 'value'], null);
|
||||
form.setFieldValue(['filters', field.name, 'operator'], null);
|
||||
}}
|
||||
options={
|
||||
filters.map((f) => {
|
||||
return {
|
||||
value: f.name,
|
||||
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item dependencies={[['filters', field.name, "field"]]}>
|
||||
{
|
||||
() => {
|
||||
const name = form.getFieldValue(['filters', field.name, "field"]);
|
||||
const type = filters.find(f => f.name === name)?.type;
|
||||
|
||||
return <Form.Item
|
||||
key={`${index}operator`}
|
||||
label={t('reportcenter.labels.advanced_filters_filter_operator')}
|
||||
name={[field.name, "operator"]}
|
||||
dependencies={[]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
options={getWhereOperatorsByType(type)}/>
|
||||
</Form.Item>
|
||||
}
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item dependencies={[['filters', field.name, "field"]]}>
|
||||
{
|
||||
() => {
|
||||
const name = form.getFieldValue(['filters', field.name, "field"]);
|
||||
const type = filters.find(f => f.name === name)?.type;
|
||||
const reflector = filters.find(f => f.name === name)?.reflector;
|
||||
|
||||
return <Form.Item
|
||||
key={`${index}value`}
|
||||
label={t('reportcenter.labels.advanced_filters_filter_value')}
|
||||
name={[field.name, "value"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{
|
||||
(() => {
|
||||
const generateReflections = (reflector) => {
|
||||
if (!reflector) return [];
|
||||
|
||||
const {name} = reflector;
|
||||
const path = name?.split('.');
|
||||
const upperPath = path?.[0];
|
||||
const finalPath = path?.slice(1).join('.');
|
||||
|
||||
return generateInternalReflections({
|
||||
bodyshop,
|
||||
upperPath,
|
||||
finalPath
|
||||
});
|
||||
};
|
||||
|
||||
const reflections = reflector ? generateReflections(reflector) : [];
|
||||
const fieldPath = [[field.name, "value"]];
|
||||
|
||||
if (reflections.length > 0) {
|
||||
return (
|
||||
<Select
|
||||
options={reflections}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue(fieldPath, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "number") {
|
||||
return (
|
||||
<InputNumber
|
||||
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/>
|
||||
);
|
||||
})()
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteFilled
|
||||
style={{margin: "1rem", paddingTop: '23px'}}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{width: "100%"}}
|
||||
>
|
||||
{t("general.actions.add")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorters Section
|
||||
* @param sorters
|
||||
* @param form
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function SortersSection({sorters, form}) {
|
||||
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}) => {
|
||||
return (
|
||||
<div>
|
||||
Sorters
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
key={`${index}field`}
|
||||
label={t('reportcenter.labels.advanced_filters_sorter_field')}
|
||||
name={[field.name, "field"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={
|
||||
sorters.map((f) => ({
|
||||
value: f.name,
|
||||
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
|
||||
}))
|
||||
}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<Form.Item
|
||||
key={`${index}direction`}
|
||||
label={t('reportcenter.labels.advanced_filters_sorter_direction')}
|
||||
name={[field.name, "direction"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={getOrderOperatorsByType()}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={2}>
|
||||
<DeleteFilled
|
||||
style={{margin: "1rem", paddingTop: '23px'}}
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{width: "100%"}}
|
||||
>
|
||||
{t("general.actions.add")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Filters
|
||||
* @param templateId
|
||||
* @param form
|
||||
* @param bodyshop
|
||||
* @returns {JSX.Element|null}
|
||||
* @constructor
|
||||
*/
|
||||
function RenderFilters({templateId, form, bodyshop}) {
|
||||
const [state, setState] = useState(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {t} = useTranslation();
|
||||
|
||||
const fetch = useCallback(async () => {
|
||||
// Reset all the filters and Sorters.
|
||||
form.resetFields(['filters']);
|
||||
form.resetFields(['sorters']);
|
||||
form.resetFields(['defaultSorters']);
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const data = await fetchFilterData({name: templateId});
|
||||
|
||||
// We have Success
|
||||
if (data?.success) {
|
||||
if (data?.data?.sorters && data?.data?.sorters.length > 0) {
|
||||
const defaultSorters = data?.data?.sorters.filter((sorter) => sorter.hasOwnProperty('default')).map((sorter) => {
|
||||
return {
|
||||
field: sorter.name,
|
||||
direction: sorter.default.direction
|
||||
};
|
||||
}).sort((a, b) => a.default.order - b.default.order);
|
||||
|
||||
form.setFieldValue('defaultSorters', JSON.stringify(defaultSorters));
|
||||
}
|
||||
// Set the state
|
||||
setState(data.data);
|
||||
}
|
||||
// Something went wrong fetching filter data
|
||||
else {
|
||||
setState(null);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, [templateId, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (templateId) {
|
||||
fetch();
|
||||
|
||||
}
|
||||
}, [templateId, fetch]);
|
||||
|
||||
const filters = useMemo(() => state?.filters || [], [state]);
|
||||
const sorters = useMemo(() => state?.sorters || [], [state]);
|
||||
|
||||
if (!templateId) return null;
|
||||
if (isLoading) return <LoadingSkeleton/>;
|
||||
if (!state) return null;
|
||||
|
||||
return (
|
||||
<div style={{marginTop: '10px'}}>
|
||||
<Checkbox
|
||||
checked={visible}
|
||||
onChange={(e) => setVisible(e.target.checked)}
|
||||
children={t('reportcenter.labels.advanced_filters')}
|
||||
/>
|
||||
{visible && (
|
||||
<div>
|
||||
{filters.length > 0 && (
|
||||
<FiltersSection filters={filters} form={form} bodyshop={bodyshop}/>
|
||||
)}
|
||||
{sorters.length > 0 && (
|
||||
<SortersSection sorters={sorters} form={form}/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import {uniqBy} from "lodash";
|
||||
|
||||
/**
|
||||
* Get value from path
|
||||
* @param obj
|
||||
* @param path
|
||||
* @returns {*}
|
||||
*/
|
||||
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
|
||||
|
||||
/**
|
||||
* Valid internal reflections
|
||||
* Note: This is intended for future functionality
|
||||
* @type {{special: string[], bodyshop: [{name: string, type: string}]}}
|
||||
*/
|
||||
const VALID_INTERNAL_REFLECTIONS = {
|
||||
bodyshop: [
|
||||
{
|
||||
name: 'md_ro_statuses.statuses',
|
||||
type: 'kv-to-v'
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate options
|
||||
* @param bodyshop
|
||||
* @param path
|
||||
* @param labelPath
|
||||
* @param valuePath
|
||||
* @returns {{label: *, value: *}[]}
|
||||
*/
|
||||
const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
|
||||
const options = getValueFromPath(bodyshop, path);
|
||||
return uniqBy(Object.values(options).map((value) => ({
|
||||
label: value[labelPath],
|
||||
value: value[valuePath],
|
||||
})), 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate special reflections
|
||||
* @param bodyshop
|
||||
* @param finalPath
|
||||
* @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
|
||||
*/
|
||||
const generateSpecialReflections = (bodyshop, finalPath) => {
|
||||
switch (finalPath) {
|
||||
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');
|
||||
case 'insurance_companies':
|
||||
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
|
||||
case 'employee_teams':
|
||||
return generateOptionsFromObject(bodyshop, 'employee_teams', 'name', 'id');
|
||||
// Special case because Employees uses a concatenation of first_name and last_name
|
||||
case 'employees':
|
||||
const employeesOptions = getValueFromPath(bodyshop, 'employees');
|
||||
return uniqBy(Object.values(employeesOptions).map((value) => ({
|
||||
label: `${value.first_name} ${value.last_name}`,
|
||||
value: value.id,
|
||||
})), 'value');
|
||||
case 'last_names':
|
||||
return generateOptionsFromObject(bodyshop, 'employees', 'last_name', 'last_name');
|
||||
case 'first_names':
|
||||
return generateOptionsFromObject(bodyshop, 'employees', 'first_name', 'first_name');
|
||||
case 'job_statuses':
|
||||
const statusOptions = getValueFromPath(bodyshop, 'md_ro_statuses.statuses');
|
||||
return Object.values(statusOptions).map((value) => ({
|
||||
label: value,
|
||||
value
|
||||
}));
|
||||
default:
|
||||
console.error('Invalid Special reflection provided by Report Filters');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate bodyshop reflections
|
||||
* @param bodyshop
|
||||
* @param finalPath
|
||||
* @returns {{label: *, value: *}[]|*[]}
|
||||
*/
|
||||
const generateBodyshopReflections = (bodyshop, finalPath) => {
|
||||
const options = getValueFromPath(bodyshop, finalPath);
|
||||
const reflectionRenderer = VALID_INTERNAL_REFLECTIONS.bodyshop.find(reflection => reflection.name === finalPath);
|
||||
if (reflectionRenderer?.type === 'kv-to-v') {
|
||||
return Object.values(options).map((value) => ({
|
||||
label: value,
|
||||
value
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate internal reflections based on the path and bodyshop
|
||||
* @param bodyshop
|
||||
* @param upperPath
|
||||
* @param finalPath
|
||||
* @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
|
||||
*/
|
||||
const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
|
||||
switch (upperPath) {
|
||||
case 'special':
|
||||
return generateSpecialReflections(bodyshop, finalPath);
|
||||
case 'bodyshop':
|
||||
return generateBodyshopReflections(bodyshop, finalPath);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export {generateInternalReflections,}
|
||||
@@ -1,70 +1,64 @@
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
Radio,
|
||||
Row,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {useLazyQuery} from "@apollo/client";
|
||||
import {Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography,} from "antd";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
|
||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||
import { selectReportCenter } from "../../redux/modals/modals.selectors";
|
||||
import DatePIckerRanges from "../../utils/DatePickerRanges";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import React, {useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {QUERY_ACTIVE_EMPLOYEES} from "../../graphql/employees.queries";
|
||||
import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries";
|
||||
import {selectReportCenter} from "../../redux/modals/modals.selectors";
|
||||
import DatePickerRanges from "../../utils/DatePickerRanges";
|
||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
||||
import {TemplateList} from "../../utils/TemplateConstants";
|
||||
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import "./report-center-modal.styles.scss";
|
||||
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reportCenterModal: selectReportCenter,
|
||||
reportCenterModal: selectReportCenter,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ReportCenterModalComponent);
|
||||
|
||||
export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
|
||||
const [form] = Form.useForm();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
const Templates = TemplateList("report_center");
|
||||
const ReportsList = Object.keys(Templates).map((key) => {
|
||||
return Templates[key];
|
||||
});
|
||||
const { visible } = reportCenterModal;
|
||||
const {open} = reportCenterModal;
|
||||
|
||||
const [callVendorQuery, { data: vendorData, called: vendorCalled }] =
|
||||
useLazyQuery(QUERY_ALL_VENDORS, {
|
||||
skip: !(
|
||||
visible &&
|
||||
Templates[form.getFieldValue("key")] &&
|
||||
Templates[form.getFieldValue("key")].idtype
|
||||
),
|
||||
});
|
||||
const [callVendorQuery, {data: vendorData, called: vendorCalled}] =
|
||||
useLazyQuery(QUERY_ALL_VENDORS, {
|
||||
skip: !(
|
||||
open &&
|
||||
Templates[form.getFieldValue("key")] &&
|
||||
Templates[form.getFieldValue("key")].idtype
|
||||
),
|
||||
});
|
||||
|
||||
const [callEmployeeQuery, { data: employeeData, called: employeeCalled }] =
|
||||
useLazyQuery(QUERY_ACTIVE_EMPLOYEES, {
|
||||
skip: !(
|
||||
visible &&
|
||||
Templates[form.getFieldValue("key")] &&
|
||||
Templates[form.getFieldValue("key")].idtype
|
||||
),
|
||||
});
|
||||
const [callEmployeeQuery, {data: employeeData, called: employeeCalled}] =
|
||||
useLazyQuery(QUERY_ACTIVE_EMPLOYEES, {
|
||||
skip: !(
|
||||
open &&
|
||||
Templates[form.getFieldValue("key")] &&
|
||||
Templates[form.getFieldValue("key")].idtype
|
||||
),
|
||||
});
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
@@ -72,244 +66,256 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
const end = values.dates ? values.dates[1] : null;
|
||||
const { id } = values;
|
||||
|
||||
await GenerateDocument(
|
||||
{
|
||||
name: values.key,
|
||||
variables: {
|
||||
...(start
|
||||
? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
|
||||
: {}),
|
||||
...(end
|
||||
? { end: moment(end).endOf("day").format("YYYY-MM-DD") }
|
||||
: {}),
|
||||
...(start ? { starttz: moment(start).startOf("day") } : {}),
|
||||
...(end ? { endtz: moment(end).endOf("day") } : {}),
|
||||
const templateConfig = {
|
||||
name: values.key,
|
||||
variables: {
|
||||
...(start
|
||||
? {start: moment(start).startOf("day").format("YYYY-MM-DD")}
|
||||
: {}),
|
||||
...(end ? {end: moment(end).endOf("day").format("YYYY-MM-DD")} : {}),
|
||||
...(start ? {starttz: moment(start).startOf("day")} : {}),
|
||||
...(end ? {endtz: moment(end).endOf("day")} : {}),
|
||||
|
||||
...(id ? { id: id } : {}),
|
||||
...(id ? {id: id} : {}),
|
||||
},
|
||||
filters: values.filters,
|
||||
sorters: values.sorters,
|
||||
};
|
||||
|
||||
if (_.isString(values.defaultSorters) && !_.isEmpty(values.defaultSorters)) {
|
||||
templateConfig.defaultSorters = JSON.parse(values.defaultSorters);
|
||||
}
|
||||
|
||||
await GenerateDocument(
|
||||
templateConfig,
|
||||
{
|
||||
to: values.to,
|
||||
subject: Templates[values.key]?.subject,
|
||||
},
|
||||
},
|
||||
{
|
||||
to: values.to,
|
||||
subject: Templates[values.key]?.subject,
|
||||
},
|
||||
values.sendbyexcel === "excel"
|
||||
? "x"
|
||||
: values.sendby === "email"
|
||||
? "e"
|
||||
: "p",
|
||||
id
|
||||
values.sendbyexcel === "excel"
|
||||
? "x"
|
||||
: values.sendby === "email"
|
||||
? "e"
|
||||
: "p",
|
||||
id
|
||||
);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const FilteredReportsList =
|
||||
search !== ""
|
||||
? ReportsList.filter((r) =>
|
||||
r.title.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: ReportsList;
|
||||
search !== ""
|
||||
? ReportsList.filter((r) =>
|
||||
r.title.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: ReportsList;
|
||||
|
||||
//Group it, create cards, and then filter out.
|
||||
|
||||
const grouped = _.groupBy(FilteredReportsList, "group");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
layout="vertical"
|
||||
form={form}
|
||||
>
|
||||
<Input.Search
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
value={search}
|
||||
/>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label={t("reportcenter.labels.key")}
|
||||
// className="radio-group-columns"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
<div>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
autoComplete={"off"}
|
||||
layout="vertical"
|
||||
form={form}
|
||||
>
|
||||
<Radio.Group>
|
||||
{/* {Object.keys(Templates).map((key) => (
|
||||
<Input.Search
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
value={search}
|
||||
/>
|
||||
<Form.Item name="defaultSorters" hidden/>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label={t("reportcenter.labels.key")}
|
||||
// className="radio-group-columns"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
{/* {Object.keys(Templates).map((key) => (
|
||||
<Radio key={key} value={key}>
|
||||
{Templates[key].title}
|
||||
</Radio>
|
||||
))} */}
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{Object.keys(grouped).map((key) => (
|
||||
<Col md={8} sm={12} key={key}>
|
||||
<Card.Grid
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
maxHeight: "33vh",
|
||||
overflowY: "scroll",
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4}>
|
||||
{t(`reportcenter.labels.groups.${key}`)}
|
||||
</Typography.Title>
|
||||
<ul style={{ columns: "2 auto" }}>
|
||||
{grouped[key].map((item) => (
|
||||
<li key={item.key}>
|
||||
<Radio key={item.key} value={item.key}>
|
||||
{item.title}
|
||||
</Radio>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card.Grid>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
if (!key) return null;
|
||||
//Kind of Id
|
||||
const rangeFilter = Templates[key] && Templates[key].rangeFilter;
|
||||
if (!rangeFilter) return null;
|
||||
return (
|
||||
<div>
|
||||
{t("reportcenter.labels.filterson", {
|
||||
object: rangeFilter.object,
|
||||
field: rangeFilter.field,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
const currentId = form.getFieldValue("id");
|
||||
if (!key) return null;
|
||||
//Kind of Id
|
||||
const idtype = Templates[key] && Templates[key].idtype;
|
||||
if (!idtype && currentId) {
|
||||
form.setFieldsValue({ id: null });
|
||||
return null;
|
||||
}
|
||||
if (!vendorCalled && idtype === "vendor") callVendorQuery();
|
||||
if (!employeeCalled && idtype === "employee") callEmployeeQuery();
|
||||
if (idtype === "vendor")
|
||||
<Row gutter={[16, 16]}>
|
||||
{Object.keys(grouped).map((key) => (
|
||||
<Col md={8} sm={12} key={key}>
|
||||
<Card.Grid
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
maxHeight: "33vh",
|
||||
overflowY: "scroll",
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4}>
|
||||
{t(`reportcenter.labels.groups.${key}`)}
|
||||
</Typography.Title>
|
||||
<ul style={{ listStyleType: 'none', columns: "2 auto"}}>
|
||||
{grouped[key].map((item) => (
|
||||
<li key={item.key}>
|
||||
<Radio key={item.key} value={item.key}>
|
||||
{item.title}
|
||||
</Radio>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card.Grid>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
if (!key) return null;
|
||||
//Kind of Id
|
||||
const rangeFilter = Templates[key] && Templates[key].rangeFilter;
|
||||
if (!rangeFilter) return null;
|
||||
return (
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={t("reportcenter.labels.vendor")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<VendorSearchSelect
|
||||
options={vendorData ? vendorData.vendors : []}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div>
|
||||
{t("reportcenter.labels.filterson", {
|
||||
object: rangeFilter.object,
|
||||
field: rangeFilter.field,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
if (idtype === "employee")
|
||||
return (
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={t("reportcenter.labels.employee")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelect
|
||||
options={employeeData ? employeeData.employees : []}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
else return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
const datedisable = Templates[key] && Templates[key].datedisable;
|
||||
if (datedisable !== true) {
|
||||
return (
|
||||
<Form.Item
|
||||
name="dates"
|
||||
label={t("reportcenter.labels.dates")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker.RangePicker
|
||||
format="MM/DD/YYYY"
|
||||
ranges={DatePIckerRanges}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
} else return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
//Kind of Id
|
||||
const reporttype = Templates[key] && Templates[key].reporttype;
|
||||
}}
|
||||
</Form.Item>
|
||||
<ReportCenterModalFiltersSortersComponent form={form} bodyshop={bodyshop} />
|
||||
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
const currentId = form.getFieldValue("id");
|
||||
if (!key) return null;
|
||||
//Kind of Id
|
||||
const idtype = Templates[key] && Templates[key].idtype;
|
||||
if (!idtype && currentId) {
|
||||
form.setFieldsValue({id: null});
|
||||
return null;
|
||||
}
|
||||
if (!vendorCalled && idtype === "vendor") callVendorQuery();
|
||||
if (!employeeCalled && idtype === "employee") callEmployeeQuery();
|
||||
if (idtype === "vendor")
|
||||
return (
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={t("reportcenter.labels.vendor")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<VendorSearchSelect
|
||||
options={vendorData ? vendorData.vendors : []}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
if (idtype === "employee")
|
||||
return (
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={t("reportcenter.labels.employee")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelect
|
||||
options={employeeData ? employeeData.employees : []}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
else return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
const datedisable = Templates[key] && Templates[key].datedisable;
|
||||
|
||||
if (reporttype === "excel")
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("general.labels.sendby")}
|
||||
name="sendbyexcel"
|
||||
initialValue="excel"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="excel">{t("general.labels.excel")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
if (reporttype !== "excel")
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("general.labels.sendby")}
|
||||
name="sendby"
|
||||
initialValue="print"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="email">{t("general.labels.email")}</Radio>
|
||||
<Radio value="print">{t("general.labels.print")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
// TODO: MERGE NOTE, Ranges turns to presets in DatePicker.RangePicker
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => form.submit()} style={{}} loading={loading}>
|
||||
{t("reportcenter.actions.generate")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
if (datedisable !== true) {
|
||||
return (
|
||||
<Form.Item
|
||||
name="dates"
|
||||
label={t("reportcenter.labels.dates")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DatePicker.RangePicker
|
||||
format="MM/DD/YYYY"
|
||||
ranges={DatePickerRanges}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
} else return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
//Kind of Id
|
||||
const reporttype = Templates[key] && Templates[key].reporttype;
|
||||
|
||||
if (reporttype === "excel")
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("general.labels.sendby")}
|
||||
name="sendbyexcel"
|
||||
initialValue="excel"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="excel">{t("general.labels.excel")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
if (reporttype !== "excel")
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("general.labels.sendby")}
|
||||
name="sendby"
|
||||
initialValue="print"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="email">{t("general.labels.email")}</Radio>
|
||||
<Radio value="print">{t("general.labels.print")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => form.submit()} style={{}} loading={loading}>
|
||||
{t("reportcenter.actions.generate")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Dropdown, Form, InputNumber, notification } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
Form,
|
||||
InputNumber,
|
||||
notification,
|
||||
Space,
|
||||
} from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
|
||||
@@ -13,6 +22,7 @@ export default function ScoreboardEntryEdit({ entry }) {
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
values.date = moment(values.date).format("YYYY-MM-DD");
|
||||
const result = await updateScoreboardentry({
|
||||
variables: { sbId: entry.id, sbInput: values },
|
||||
});
|
||||
@@ -77,13 +87,14 @@ export default function ScoreboardEntryEdit({ entry }) {
|
||||
>
|
||||
<InputNumber precision={1} />
|
||||
</Form.Item>
|
||||
|
||||
<Button type="primary" loading={loading} htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisible(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
<Space wrap>
|
||||
<Button type="primary" loading={loading} htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisible(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Card, Input, Modal, Space, Table, Typography } from "antd";
|
||||
import React, { useState } from "react";
|
||||
@@ -5,12 +6,14 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
|
||||
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
@@ -44,6 +47,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.job.id}>
|
||||
{record.job.ro_number || t("general.labels.na")}
|
||||
@@ -55,7 +59,11 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
OwnerNameDisplayFunction(a.job),
|
||||
OwnerNameDisplayFunction(b.job)
|
||||
),
|
||||
render: (text, record) => <OwnerNameDisplay ownerObject={record.job} />,
|
||||
},
|
||||
{
|
||||
@@ -63,6 +71,15 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) =>
|
||||
alphaSort(
|
||||
`${a.job.v_model_yr || ""} ${a.job.v_make_desc || ""} ${
|
||||
a.job.v_model_desc || ""
|
||||
}`,
|
||||
`${b.job.v_model_yr || ""} ${b.job.v_make_desc || ""} ${
|
||||
b.job.v_model_desc || ""
|
||||
}`
|
||||
),
|
||||
render: (text, record) => (
|
||||
<span>{`${record.job.v_model_yr || ""} ${
|
||||
record.job.v_make_desc || ""
|
||||
@@ -73,17 +90,20 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
title: t("scoreboard.fields.date"),
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
sorter: (a, b) => dateSort(a.date, b.date),
|
||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.fields.painthrs"),
|
||||
dataIndex: "painthrs",
|
||||
key: "painthrs",
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.fields.bodyhrs"),
|
||||
dataIndex: "bodyhrs",
|
||||
key: "bodyhrs",
|
||||
sorter: (a, b) => Number(a.bodyhrs) - Number(b.bodyhrs),
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.fields.painthrs"),
|
||||
dataIndex: "painthrs",
|
||||
key: "painthrs",
|
||||
sorter: (a, b) => Number(a.painthrs) - Number(b.painthrs),
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
@@ -104,8 +124,9 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||
visible={state.visible}
|
||||
destroyOnClose
|
||||
width="80%"
|
||||
closable={false}
|
||||
cancelButtonProps={{ style: { display: "none" } }}
|
||||
onCancel={() =>
|
||||
onOk={() =>
|
||||
setState((state) => ({
|
||||
...state,
|
||||
visible: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Form } from "antd";
|
||||
import React from "react";
|
||||
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
||||
|
||||
export default function ShopCsiConfigForm({ selectedCsi }) {
|
||||
@@ -9,7 +9,7 @@ export default function ShopCsiConfigForm({ selectedCsi }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
The Config Form {readOnly}
|
||||
{readOnly}
|
||||
{selectedCsi && (
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<ConfigFormComponents
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CheckCircleFilled } from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Col, List, Row } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GET_ALL_QUESTION_SETS } from "../../graphql/csi.queries";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
@@ -21,7 +21,6 @@ export default function ShopCsiConfig() {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<div>
|
||||
The Config Form
|
||||
<Row>
|
||||
<Col span={3}>
|
||||
<List
|
||||
@@ -42,7 +41,8 @@ export default function ShopCsiConfig() {
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Col span={1} />
|
||||
<Col span={20}>
|
||||
<ShopCsiConfigForm selectedCsi={selectedCsi} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -22,6 +22,7 @@ export const QUERY_AVAILABLE_CC = gql`
|
||||
]
|
||||
status: { _eq: "courtesycars.status.in" }
|
||||
}
|
||||
order_by: { fleetnumber: asc }
|
||||
) {
|
||||
color
|
||||
dailycost
|
||||
@@ -29,6 +30,7 @@ export const QUERY_AVAILABLE_CC = gql`
|
||||
fleetnumber
|
||||
fuel
|
||||
id
|
||||
insuranceexpires
|
||||
make
|
||||
mileage
|
||||
model
|
||||
@@ -57,7 +59,7 @@ export const CHECK_CC_FLEET_NUMBER = gql`
|
||||
`;
|
||||
export const QUERY_ALL_CC = gql`
|
||||
query QUERY_ALL_CC {
|
||||
courtesycars {
|
||||
courtesycars(order_by: { fleetnumber: asc }) {
|
||||
color
|
||||
created_at
|
||||
dailycost
|
||||
|
||||
@@ -57,19 +57,15 @@ export const INSERT_CSI = gql`
|
||||
`;
|
||||
|
||||
export const QUERY_CSI_RESPONSE_PAGINATED = gql`
|
||||
query QUERY_CSI_RESPONSE_PAGINATED(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$order: [csi_order_by!]!
|
||||
) {
|
||||
csi(offset: $offset, limit: $limit, order_by: $order) {
|
||||
query QUERY_CSI_RESPONSE_PAGINATED {
|
||||
csi(order_by: { completedon: desc_nulls_last }) {
|
||||
id
|
||||
completedon
|
||||
job {
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownerid
|
||||
ro_number
|
||||
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -83,6 +79,7 @@ export const QUERY_CSI_RESPONSE_PAGINATED = gql`
|
||||
export const QUERY_CSI_RESPONSE_BY_PK = gql`
|
||||
query QUERY_CSI_RESPONSE_BY_PK($id: uuid!) {
|
||||
csi_by_pk(id: $id) {
|
||||
completedon
|
||||
relateddata
|
||||
valid
|
||||
validuntil
|
||||
|
||||
@@ -108,12 +108,7 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
|
||||
`;
|
||||
|
||||
export const QUERY_PARTS_QUEUE = gql`
|
||||
query QUERY_PARTS_QUEUE(
|
||||
$statuses: [String!]!
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$order: [jobs_order_by!]
|
||||
) {
|
||||
query QUERY_PARTS_QUEUE($statuses: [String!]!, $offset: Int, $limit: Int) {
|
||||
jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) {
|
||||
aggregate {
|
||||
count(distinct: true)
|
||||
@@ -125,7 +120,7 @@ export const QUERY_PARTS_QUEUE = gql`
|
||||
}
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
order_by: { ro_number: desc }
|
||||
) {
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
@@ -142,7 +137,9 @@ export const QUERY_PARTS_QUEUE = gql`
|
||||
v_color
|
||||
vehicleid
|
||||
scheduled_in
|
||||
scheduled_completion
|
||||
id
|
||||
ins_co_nm
|
||||
clm_no
|
||||
ro_number
|
||||
status
|
||||
@@ -2338,3 +2335,163 @@ export const MARK_JOB_AS_UNINVOICED = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||
query QUERY_JOB_CARD_DETAILS($id: uuid!) {
|
||||
jobs_by_pk(id: $id) {
|
||||
actual_completion
|
||||
actual_delivery
|
||||
actual_in
|
||||
alt_transport
|
||||
available_jobs {
|
||||
id
|
||||
}
|
||||
area_of_damage
|
||||
ca_gst_registrant
|
||||
cccontracts {
|
||||
agreementnumber
|
||||
courtesycar {
|
||||
id
|
||||
make
|
||||
model
|
||||
year
|
||||
plate
|
||||
fleetnumber
|
||||
}
|
||||
id
|
||||
scheduledreturn
|
||||
start
|
||||
status
|
||||
}
|
||||
clm_no
|
||||
clm_total
|
||||
comment
|
||||
date_estimated
|
||||
date_exported
|
||||
date_invoiced
|
||||
date_last_contacted
|
||||
date_next_contact
|
||||
date_open
|
||||
date_repairstarted
|
||||
date_scheduled
|
||||
ded_amt
|
||||
employee_body
|
||||
employee_body_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
employee_csr
|
||||
employee_csr_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
employee_prep
|
||||
employee_prep_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
employee_refinish
|
||||
employee_refinish_rel {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
}
|
||||
est_co_nm
|
||||
est_ct_fn
|
||||
est_ct_ln
|
||||
est_ea
|
||||
est_ph1
|
||||
id
|
||||
ins_co_nm
|
||||
ins_ct_fn
|
||||
ins_ct_ln
|
||||
ins_ea
|
||||
ins_ph1
|
||||
inproduction
|
||||
job_totals
|
||||
joblines(
|
||||
order_by: { line_no: asc }
|
||||
where: {
|
||||
part_type: {
|
||||
_in: [
|
||||
"PAN"
|
||||
"PAC"
|
||||
"PAR"
|
||||
"PAL"
|
||||
"PAA"
|
||||
"PAM"
|
||||
"PAP"
|
||||
"PAG"
|
||||
]
|
||||
}
|
||||
removed: { _eq: false }
|
||||
}
|
||||
) {
|
||||
act_price
|
||||
alt_partno
|
||||
db_ref
|
||||
id
|
||||
line_desc
|
||||
line_no
|
||||
location
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
oem_partno
|
||||
part_qty
|
||||
part_type
|
||||
prt_dsmk_m
|
||||
status
|
||||
}
|
||||
lbr_adjustments
|
||||
ownr_co_nm
|
||||
ownr_ea
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
owner {
|
||||
id
|
||||
allow_text_message
|
||||
preferred_contact
|
||||
tax_number
|
||||
}
|
||||
owner_owing
|
||||
plate_no
|
||||
plate_st
|
||||
po_number
|
||||
production_vars
|
||||
ro_number
|
||||
scheduled_completion
|
||||
scheduled_delivery
|
||||
scheduled_in
|
||||
special_coverage_policy
|
||||
status
|
||||
suspended
|
||||
updated_at
|
||||
vehicle {
|
||||
id
|
||||
jobs {
|
||||
id
|
||||
clm_no
|
||||
ro_number
|
||||
}
|
||||
notes
|
||||
plate_no
|
||||
v_color
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
v_model_yr
|
||||
}
|
||||
vehicleid
|
||||
v_color
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
v_model_yr
|
||||
v_vin
|
||||
voided
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,88 +1,67 @@
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { Form, Layout, Typography, Button, Result } from "antd";
|
||||
import React, { useState } from "react";
|
||||
// import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Form, Layout, Result, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import ConfigFormComponents from "../../components/config-form-components/config-form-components.component";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
import { QUERY_SURVEY, COMPLETE_SURVEY } from "../../graphql/csi.queries";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormat } from "./../../utils/DateFormatter";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage);
|
||||
|
||||
export function CsiContainerPage({ currentUser }) {
|
||||
const { surveyId } = useParams();
|
||||
const [form] = Form.useForm();
|
||||
const [axiosResponse, setAxiosResponse] = useState(null);
|
||||
const [submitting, setSubmitting] = useState({
|
||||
loading: false,
|
||||
submitted: false,
|
||||
});
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY_SURVEY, {
|
||||
variables: { surveyId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [completeSurvey] = useMutation(COMPLETE_SURVEY);
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
if (error || !!!data.csi_by_pk)
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
status="error"
|
||||
title={t("csi.errors.notfoundtitle")}
|
||||
subTitle={t("csi.errors.notfoundsubtitle")}
|
||||
>
|
||||
{error ? (
|
||||
<div>ERROR: {error.graphQLErrors.map((e) => e.message)}</div>
|
||||
) : null}
|
||||
</Result>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setSubmitting({ ...submitting, loading: true });
|
||||
|
||||
const result = await completeSurvey({
|
||||
variables: {
|
||||
surveyId,
|
||||
survey: {
|
||||
response: values,
|
||||
valid: false,
|
||||
completedon: new Date(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!!!result.errors) {
|
||||
setSubmitting({ ...submitting, loading: false, submitted: true });
|
||||
} else {
|
||||
setSubmitting({
|
||||
...submitting,
|
||||
const getAxiosData = useCallback(async () => {
|
||||
try {
|
||||
try {
|
||||
window.$crisp.push(["do", "chat:hide"]);
|
||||
} catch {
|
||||
console.log("Unable to attach to crisp instance. ");
|
||||
}
|
||||
setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true }));
|
||||
const response = await axios.post("/csi/lookup", { surveyId });
|
||||
setSubmitting((prevSubmitting) => ({
|
||||
...prevSubmitting,
|
||||
loading: false,
|
||||
error: JSON.stringify(result.errors),
|
||||
}));
|
||||
setAxiosResponse(response.data);
|
||||
} catch (error) {
|
||||
console.error(`Something went wrong...: ${error.message}`);
|
||||
console.dir({
|
||||
stack: error?.stack,
|
||||
message: error?.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [setAxiosResponse, surveyId]);
|
||||
|
||||
const {
|
||||
relateddata: { bodyshop, job },
|
||||
csiquestion: { config: csiquestions },
|
||||
} = data.csi_by_pk;
|
||||
useEffect(() => {
|
||||
getAxiosData().catch((err) =>
|
||||
console.error(
|
||||
`Something went wrong fetching axios data: ${err.message || ""}`
|
||||
)
|
||||
);
|
||||
}, [getAxiosData]);
|
||||
|
||||
if (currentUser && currentUser.authorized)
|
||||
// Return if authorized
|
||||
if (currentUser && currentUser.authorized) {
|
||||
return (
|
||||
<Layout
|
||||
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
|
||||
@@ -94,85 +73,176 @@ export function CsiContainerPage({ currentUser }) {
|
||||
/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
|
||||
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
|
||||
<img src={bodyshop.logo_img_path.src} alt="Logo" />
|
||||
) : null}
|
||||
<div style={{ margin: "2em" }}>
|
||||
<strong>{bodyshop.shopname || ""}</strong>
|
||||
<div>{`${bodyshop.address1 || ""}`}</div>
|
||||
<div>{`${bodyshop.address2 || ""}`}</div>
|
||||
<div>{`${bodyshop.city || ""} ${bodyshop.state || ""} ${
|
||||
bodyshop.zip_post || ""
|
||||
}`}</div>
|
||||
if (submitting.loading) return <LoadingSpinner />;
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
try {
|
||||
setSubmitting({ ...submitting, loading: true, submitting: true });
|
||||
const result = await axios.post("/csi/submit", { surveyId, values });
|
||||
console.log("result", result);
|
||||
if (!!!result.errors && result.data.update_csi.affected_rows > 0) {
|
||||
setSubmitting({ ...submitting, loading: false, submitted: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Something went wrong...: ${error.message}`);
|
||||
console.dir({
|
||||
stack: error?.stack,
|
||||
message: error?.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!axiosResponse || axiosResponse.csi_by_pk === null) {
|
||||
// Do something here , this is where you would return a loading box or something
|
||||
return (
|
||||
<>
|
||||
<Layout style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Form>
|
||||
<Result
|
||||
status="error"
|
||||
title={t("csi.errors.notfoundtitle")}
|
||||
subTitle={t("csi.errors.notfoundsubtitle")}
|
||||
/>
|
||||
</Form>
|
||||
</Layout.Content>
|
||||
<Layout.Footer>
|
||||
{t("csi.labels.copyright")}{" "}
|
||||
{t("csi.fields.surveyid", { surveyId: surveyId })}
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const {
|
||||
relateddata: { bodyshop, job },
|
||||
csiquestion: { config: csiquestions },
|
||||
} = axiosResponse.csi_by_pk;
|
||||
|
||||
return (
|
||||
<Layout style={{ display: "flex", flexDirection: "column" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
|
||||
{bodyshop.logo_img_path && bodyshop.logo_img_path.src ? (
|
||||
<img
|
||||
src={bodyshop.logo_img_path.src}
|
||||
alt={bodyshop.shopname.concat(" Logo")}
|
||||
height={bodyshop.logo_img_path.height}
|
||||
width={bodyshop.logo_img_path.width}
|
||||
/>
|
||||
) : null}
|
||||
<div style={{ margin: "2em", verticalAlign: "middle" }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
{bodyshop.shopname || ""}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph style={{ margin: 0 }}>
|
||||
{`${bodyshop.address1 || ""}${bodyshop.address2 ? ", " : ""}${
|
||||
bodyshop.address2 || ""
|
||||
}`.trim()}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph style={{ margin: 0 }}>
|
||||
{`${bodyshop.city || ""}${
|
||||
bodyshop.city && bodyshop.state ? ", " : ""
|
||||
}${bodyshop.state || ""} ${bodyshop.zip_post || ""}`.trim()}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
|
||||
<strong>
|
||||
{t("csi.labels.greeting", {
|
||||
name: job.ownr_co_nm || job.ownr_fn || "",
|
||||
})}
|
||||
</strong>
|
||||
<Typography.Paragraph>
|
||||
{t("csi.labels.intro", { shopname: bodyshop.shopname || "" })}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
|
||||
<strong>{`Hi ${job.ownr_co_nm || job.ownr_fn || ""}!`}</strong>
|
||||
<Typography.Paragraph>
|
||||
{`At ${
|
||||
bodyshop.shopname || ""
|
||||
}, we value your feedback. We would love to
|
||||
hear what you have to say. Please fill out the form below.`}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
{submitting.error ? (
|
||||
<AlertComponent message={submitting.error} type="error" />
|
||||
) : null}
|
||||
{submitting.error ? (
|
||||
<AlertComponent message={submitting.error} type="error" />
|
||||
) : null}
|
||||
|
||||
{submitting.submitted ? (
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
status="success"
|
||||
title={t("csi.successes.submitted")}
|
||||
subTitle={t("csi.successes.submittedsub")}
|
||||
/>
|
||||
</Layout.Content>
|
||||
) : (
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<ConfigFormComponents componentList={csiquestions} />
|
||||
<Button
|
||||
loading={submitting.loading}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
>
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
</Form>
|
||||
</Layout.Content>
|
||||
)}
|
||||
|
||||
<Layout.Footer>
|
||||
{`Copyright ImEX.Online. Survey ID: ${surveyId}`}
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
);
|
||||
{submitting.submitted ? (
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
status="success"
|
||||
title={t("csi.successes.submitted")}
|
||||
subTitle={t("csi.successes.submittedsub")}
|
||||
/>
|
||||
</Layout.Content>
|
||||
) : (
|
||||
<Layout.Content
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
{axiosResponse.csi_by_pk.valid ? (
|
||||
<>
|
||||
<ConfigFormComponents componentList={csiquestions} />
|
||||
<Button
|
||||
loading={submitting.loading}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{ float: "right" }}
|
||||
>
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Result
|
||||
title={t("csi.errors.surveycompletetitle")}
|
||||
status="warning"
|
||||
subTitle={t("csi.errors.surveycompletesubtitle", {
|
||||
date: DateTimeFormat(axiosResponse.csi_by_pk.completedon),
|
||||
})}
|
||||
/>
|
||||
<Typography.Paragraph
|
||||
type="secondary"
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{t("csi.successes.submittedsub")}
|
||||
</Typography.Paragraph>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Layout.Content>
|
||||
)}
|
||||
<Layout.Footer>
|
||||
{t("csi.labels.copyright")}{" "}
|
||||
{t("csi.fields.surveyid", { surveyId: surveyId })}
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,12 @@ import { OwnerNameDisplayFunction } from "../../components/owner-name-display/ow
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
|
||||
import {
|
||||
insertAuditTrail,
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -38,6 +40,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||
@@ -46,7 +50,7 @@ export const socket = SocketIO(
|
||||
process.env.NODE_ENV === "production"
|
||||
? process.env.REACT_APP_AXIOS_BASE_API_URL
|
||||
: window.location.origin,
|
||||
// "http://localhost:4000", // for dev testing,
|
||||
// "http://localhost:4000", // for dev testing,
|
||||
{
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
@@ -57,7 +61,12 @@ export const socket = SocketIO(
|
||||
}
|
||||
);
|
||||
|
||||
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
export function DmsContainer({
|
||||
bodyshop,
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [logLevel, setLogLevel] = useState("DEBUG");
|
||||
const history = useHistory();
|
||||
@@ -115,6 +124,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
notification.success({
|
||||
message: t("jobs.successes.exported"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: payload,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
});
|
||||
history.push("/manage/accounting/receivables");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import PartsQueueDetailCard from "../../components/parts-queue-card/parts-queue-card.component";
|
||||
import PartsQueueList from "../../components/parts-queue-list/parts-queue.list.component";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import PartsQueuePage from "./parts-queue.page.component";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
@@ -26,7 +27,8 @@ export function PartsQueuePageContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
|
||||
return (
|
||||
<RbacWrapper action="jobs:partsqueue">
|
||||
<PartsQueuePage />
|
||||
<PartsQueueList />
|
||||
<PartsQueueDetailCard />
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { Row, Col } from "antd";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
import { Col, Row } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import CsiResponseFormContainer from "../../components/csi-response-form/csi-response-form.container";
|
||||
import CsiResponseListPaginated from "../../components/csi-response-list-paginated/csi-response-list-paginated.component";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import { QUERY_CSI_RESPONSE_PAGINATED } from "../../graphql/csi.queries";
|
||||
import {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import {pageLimit} from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
@@ -33,28 +31,11 @@ export function ShopCsiContainer({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { page, sortcolumn, sortorder } = searchParams;
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(
|
||||
QUERY_CSI_RESPONSE_PAGINATED,
|
||||
{
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
//search: search || "",
|
||||
offset: page ? (page - 1) * pageLimit : 0,
|
||||
limit: pageLimit,
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "completedon"]: sortorder
|
||||
? sortorder === "descend"
|
||||
? "desc_nulls_last"
|
||||
: "asc"
|
||||
: "desc_nulls_last",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -73,12 +54,7 @@ export function ShopCsiContainer({
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<RbacWrapper
|
||||
action="csi:page"
|
||||
// noauth={
|
||||
// <AlertComponent message="You don't have acess to see this screen." />
|
||||
// }
|
||||
>
|
||||
<RbacWrapper action="csi:page">
|
||||
<Row gutter={16}>
|
||||
<Col span={10}>
|
||||
<CsiResponseListPaginated
|
||||
|
||||
@@ -289,7 +289,7 @@ export function* insertAuditTrailSaga({
|
||||
fields: {
|
||||
audit_trail(existingAuditTrail, { readField }) {
|
||||
const newAuditTrail = cache.writeQuery({
|
||||
data: data.insert_audit_trail_one,
|
||||
data: data,
|
||||
query: INSERT_AUDIT_TRAIL,
|
||||
variables,
|
||||
});
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"alerttoggle": "Alert Toggle set to {{status}}",
|
||||
"appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.",
|
||||
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
|
||||
"billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
|
||||
"billposted": "Bill with invoice number {{invoice_number}} posted.",
|
||||
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
|
||||
"failedpayment": "Failed payment",
|
||||
@@ -114,6 +115,7 @@
|
||||
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
|
||||
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
|
||||
"jobconverted": "Job converted and assigned number {{ro_number}}.",
|
||||
"jobexported": "Job has been exported.",
|
||||
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
|
||||
"jobimported": "Job imported.",
|
||||
"jobinproductionchange": "Job production status set to {{inproduction}}",
|
||||
@@ -126,7 +128,9 @@
|
||||
"jobspartsorder": "Parts order {{order_number}} added to Job.",
|
||||
"jobspartsreturn": "Parts return {{order_number}} added to Job.",
|
||||
"jobstatuschange": "Job status changed to {{status}}.",
|
||||
"jobsupplement": "Job supplement imported."
|
||||
"jobsupplement": "Job supplement imported.",
|
||||
"jobsuspend": "Suspend Toggle set to {{status}}",
|
||||
"jobvoid": "Job has been voided."
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
@@ -747,6 +751,7 @@
|
||||
"driverinformation": "Driver's Information",
|
||||
"findcontract": "Find Contract",
|
||||
"findermodal": "Contract Finder",
|
||||
"insuranceexpired": "The courtesy car insurance expires before the car is expected to return.",
|
||||
"noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.",
|
||||
"populatefromjob": "Populate from Job",
|
||||
"rates": "Contract Rates",
|
||||
@@ -838,20 +843,27 @@
|
||||
"creating": "Error creating survey {{message}}",
|
||||
"notconfigured": "You do not have any current CSI Question Sets configured.",
|
||||
"notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.",
|
||||
"notfoundtitle": "No survey found."
|
||||
"notfoundtitle": "No survey found.",
|
||||
"surveycompletetitle": "Survey previously completed",
|
||||
"surveycompletesubtitle": "This survey was already completed on {{date}}."
|
||||
},
|
||||
"fields": {
|
||||
"completedon": "Completed On",
|
||||
"created_at": "Created At"
|
||||
"created_at": "Created At",
|
||||
"surveyid": "Survey ID {{surveyId}}",
|
||||
"validuntil": "Valid Until"
|
||||
},
|
||||
"labels": {
|
||||
"nologgedinuser": "Please log out of ImEX Online",
|
||||
"nologgedinuser_sub": "Users of ImEX Online cannot complete CSI surveys while logged in. Please log out and try again.",
|
||||
"nologgedinuser": "Please log out of $t(titles.app)",
|
||||
"nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.",
|
||||
"noneselected": "No response selected.",
|
||||
"title": "Customer Satisfaction Survey"
|
||||
"title": "Customer Satisfaction Survey",
|
||||
"greeting": "Hi {{name}}!",
|
||||
"intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.",
|
||||
"copyright": "Copyright © $t(titles.app). All Rights Reserved."
|
||||
},
|
||||
"successes": {
|
||||
"created": "CSI created successfully. ",
|
||||
"created": "CSI created successfully.",
|
||||
"submitted": "Your responses have been submitted successfully.",
|
||||
"submittedsub": "Your input is highly appreciated."
|
||||
}
|
||||
@@ -867,6 +879,7 @@
|
||||
"labels": {
|
||||
"bodyhrs": "Body Hrs",
|
||||
"dollarsinproduction": "Dollars in Production",
|
||||
"phone": "Phone",
|
||||
"prodhrs": "Production Hrs",
|
||||
"refhrs": "Refinish Hrs"
|
||||
},
|
||||
@@ -882,8 +895,10 @@
|
||||
"productiondollars": "Total Dollars in Production",
|
||||
"productionhours": "Total Hours in Production",
|
||||
"projectedmonthlysales": "Projected Monthly Sales",
|
||||
"scheduledintoday": "Sheduled In Today: {{date}}",
|
||||
"scheduledouttoday": "Sheduled Out Today: {{date}}"
|
||||
"scheduledindate": "Sheduled In Today: {{date}}",
|
||||
"scheduledintoday": "Sheduled In Today",
|
||||
"scheduledoutdate": "Sheduled Out Today: {{date}}",
|
||||
"scheduledouttoday": "Sheduled Out Today"
|
||||
}
|
||||
},
|
||||
"dms": {
|
||||
@@ -1725,6 +1740,7 @@
|
||||
"ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ",
|
||||
"calc_repair_days": "Calculated Repair Days",
|
||||
"calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).",
|
||||
"calc_scheuled_completion": "Calculate Scheduled Completion",
|
||||
"cards": {
|
||||
"customer": "Customer Information",
|
||||
"damage": "Area of Damage",
|
||||
@@ -1822,6 +1838,7 @@
|
||||
"override_header": "Override estimate header on import?",
|
||||
"ownerassociation": "Owner Association",
|
||||
"parts": "Parts",
|
||||
"parts_lines": "Parts Lines",
|
||||
"parts_received": "Parts Rec.",
|
||||
"parts_tax_rates": "Parts Tax rates",
|
||||
"partsfilter": "Parts Only",
|
||||
@@ -1883,6 +1900,7 @@
|
||||
"total_sales": "Total Sales",
|
||||
"totals": "Totals",
|
||||
"unvoidnote": "This Job was unvoided.",
|
||||
"update_scheduled_completion": "Update Scheduled Completion?",
|
||||
"vehicle_info": "Vehicle",
|
||||
"vehicleassociation": "Vehicle Association",
|
||||
"viewallocations": "View Allocations",
|
||||
@@ -2564,6 +2582,16 @@
|
||||
"generate": "Generate"
|
||||
},
|
||||
"labels": {
|
||||
"advanced_filters": "Advanced Filters and Sorters",
|
||||
"advanced_filters_show": "Show",
|
||||
"advanced_filters_hide": "Hide",
|
||||
"advanced_filters_filters": "Filters",
|
||||
"advanced_filters_sorters": "Sorters",
|
||||
"advanced_filters_filter_field": "Field",
|
||||
"advanced_filters_sorter_field": "Field",
|
||||
"advanced_filters_sorter_direction": "Direction",
|
||||
"advanced_filters_filter_operator": "Operator",
|
||||
"advanced_filters_filter_value": "Value",
|
||||
"dates": "Dates",
|
||||
"employee": "Employee",
|
||||
"filterson": "Filters on {{object}}: {{field}}",
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"alerttoggle": "",
|
||||
"appointmentcancel": "",
|
||||
"appointmentinsert": "",
|
||||
"billdeleted": "",
|
||||
"billposted": "",
|
||||
"billupdated": "",
|
||||
"failedpayment": "",
|
||||
@@ -114,6 +115,7 @@
|
||||
"jobassignmentremoved": "",
|
||||
"jobchecklist": "",
|
||||
"jobconverted": "",
|
||||
"jobexported": "",
|
||||
"jobfieldchanged": "",
|
||||
"jobimported": "",
|
||||
"jobinproductionchange": "",
|
||||
@@ -126,7 +128,9 @@
|
||||
"jobspartsorder": "",
|
||||
"jobspartsreturn": "",
|
||||
"jobstatuschange": "",
|
||||
"jobsupplement": ""
|
||||
"jobsupplement": "",
|
||||
"jobsuspend": "",
|
||||
"jobvoid": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
@@ -747,6 +751,7 @@
|
||||
"driverinformation": "",
|
||||
"findcontract": "",
|
||||
"findermodal": "",
|
||||
"insuranceexpired": "",
|
||||
"noteconvertedfrom": "",
|
||||
"populatefromjob": "",
|
||||
"rates": "",
|
||||
@@ -838,17 +843,24 @@
|
||||
"creating": "",
|
||||
"notconfigured": "",
|
||||
"notfoundsubtitle": "",
|
||||
"notfoundtitle": ""
|
||||
"notfoundtitle": "",
|
||||
"surveycompletetitle": "",
|
||||
"surveycompletesubtitle": ""
|
||||
},
|
||||
"fields": {
|
||||
"completedon": "",
|
||||
"created_at": ""
|
||||
"created_at": "",
|
||||
"surveyid": "",
|
||||
"validuntil": ""
|
||||
},
|
||||
"labels": {
|
||||
"nologgedinuser": "",
|
||||
"nologgedinuser_sub": "",
|
||||
"noneselected": "",
|
||||
"title": ""
|
||||
"title": "",
|
||||
"greeting": "",
|
||||
"intro": "",
|
||||
"copyright": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -867,6 +879,7 @@
|
||||
"labels": {
|
||||
"bodyhrs": "",
|
||||
"dollarsinproduction": "",
|
||||
"phone": "",
|
||||
"prodhrs": "",
|
||||
"refhrs": ""
|
||||
},
|
||||
@@ -882,7 +895,9 @@
|
||||
"productiondollars": "",
|
||||
"productionhours": "",
|
||||
"projectedmonthlysales": "",
|
||||
"scheduledindate": "",
|
||||
"scheduledintoday": "",
|
||||
"scheduledoutdate": "",
|
||||
"scheduledouttoday": ""
|
||||
}
|
||||
},
|
||||
@@ -1725,6 +1740,7 @@
|
||||
"ca_gst_all_if_null": "",
|
||||
"calc_repair_days": "",
|
||||
"calc_repair_days_tt": "",
|
||||
"calc_scheuled_completion": "",
|
||||
"cards": {
|
||||
"customer": "Información al cliente",
|
||||
"damage": "Área de Daño",
|
||||
@@ -1822,6 +1838,7 @@
|
||||
"override_header": "¿Anular encabezado estimado al importar?",
|
||||
"ownerassociation": "",
|
||||
"parts": "Partes",
|
||||
"parts_lines": "",
|
||||
"parts_received": "",
|
||||
"parts_tax_rates": "",
|
||||
"partsfilter": "",
|
||||
@@ -1883,6 +1900,7 @@
|
||||
"total_sales": "",
|
||||
"totals": "",
|
||||
"unvoidnote": "",
|
||||
"update_scheduled_completion": "",
|
||||
"vehicle_info": "Vehículo",
|
||||
"vehicleassociation": "",
|
||||
"viewallocations": "",
|
||||
@@ -2564,6 +2582,16 @@
|
||||
"generate": ""
|
||||
},
|
||||
"labels": {
|
||||
"advanced_filters": "",
|
||||
"advanced_filters_show": "",
|
||||
"advanced_filters_hide": "",
|
||||
"advanced_filters_filters": "",
|
||||
"advanced_filters_sorters": "",
|
||||
"advanced_filters_filter_field": "",
|
||||
"advanced_filters_sorter_field": "",
|
||||
"advanced_filters_sorter_direction": "",
|
||||
"advanced_filters_filter_operator": "",
|
||||
"advanced_filters_filter_value": "",
|
||||
"dates": "",
|
||||
"employee": "",
|
||||
"filterson": "",
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"alerttoggle": "",
|
||||
"appointmentcancel": "",
|
||||
"appointmentinsert": "",
|
||||
"billdeleted": "",
|
||||
"billposted": "",
|
||||
"billupdated": "",
|
||||
"failedpayment": "",
|
||||
@@ -114,6 +115,7 @@
|
||||
"jobassignmentremoved": "",
|
||||
"jobchecklist": "",
|
||||
"jobconverted": "",
|
||||
"jobexported": "",
|
||||
"jobfieldchanged": "",
|
||||
"jobimported": "",
|
||||
"jobinproductionchange": "",
|
||||
@@ -126,7 +128,9 @@
|
||||
"jobspartsorder": "",
|
||||
"jobspartsreturn": "",
|
||||
"jobstatuschange": "",
|
||||
"jobsupplement": ""
|
||||
"jobsupplement": "",
|
||||
"jobsuspend": "",
|
||||
"jobvoid": ""
|
||||
}
|
||||
},
|
||||
"billlines": {
|
||||
@@ -747,6 +751,7 @@
|
||||
"driverinformation": "",
|
||||
"findcontract": "",
|
||||
"findermodal": "",
|
||||
"insuranceexpired": "",
|
||||
"noteconvertedfrom": "",
|
||||
"populatefromjob": "",
|
||||
"rates": "",
|
||||
@@ -838,17 +843,24 @@
|
||||
"creating": "",
|
||||
"notconfigured": "",
|
||||
"notfoundsubtitle": "",
|
||||
"notfoundtitle": ""
|
||||
"notfoundtitle": "",
|
||||
"surveycompletetitle": "",
|
||||
"surveycompletesubtitle": ""
|
||||
},
|
||||
"fields": {
|
||||
"completedon": "",
|
||||
"created_at": ""
|
||||
"created_at": "",
|
||||
"surveyid": "",
|
||||
"validuntil": ""
|
||||
},
|
||||
"labels": {
|
||||
"nologgedinuser": "",
|
||||
"nologgedinuser_sub": "",
|
||||
"noneselected": "",
|
||||
"title": ""
|
||||
"title": "",
|
||||
"greeting": "",
|
||||
"intro": "",
|
||||
"copyright": ""
|
||||
},
|
||||
"successes": {
|
||||
"created": "",
|
||||
@@ -867,6 +879,7 @@
|
||||
"labels": {
|
||||
"bodyhrs": "",
|
||||
"dollarsinproduction": "",
|
||||
"phone": "",
|
||||
"prodhrs": "",
|
||||
"refhrs": ""
|
||||
},
|
||||
@@ -882,7 +895,9 @@
|
||||
"productiondollars": "",
|
||||
"productionhours": "",
|
||||
"projectedmonthlysales": "",
|
||||
"scheduledindate": "",
|
||||
"scheduledintoday": "",
|
||||
"scheduledoutdate": "",
|
||||
"scheduledouttoday": ""
|
||||
}
|
||||
},
|
||||
@@ -1725,6 +1740,7 @@
|
||||
"ca_gst_all_if_null": "",
|
||||
"calc_repair_days": "",
|
||||
"calc_repair_days_tt": "",
|
||||
"calc_scheuled_completion": "",
|
||||
"cards": {
|
||||
"customer": "Informations client",
|
||||
"damage": "Zone de dommages",
|
||||
@@ -1822,6 +1838,7 @@
|
||||
"override_header": "Remplacer l'en-tête d'estimation à l'importation?",
|
||||
"ownerassociation": "",
|
||||
"parts": "les pièces",
|
||||
"parts_lines": "",
|
||||
"parts_received": "",
|
||||
"parts_tax_rates": "",
|
||||
"partsfilter": "",
|
||||
@@ -1883,6 +1900,7 @@
|
||||
"total_sales": "",
|
||||
"totals": "",
|
||||
"unvoidnote": "",
|
||||
"update_scheduled_completion": "",
|
||||
"vehicle_info": "Véhicule",
|
||||
"vehicleassociation": "",
|
||||
"viewallocations": "",
|
||||
@@ -2564,6 +2582,16 @@
|
||||
"generate": ""
|
||||
},
|
||||
"labels": {
|
||||
"advanced_filters": "",
|
||||
"advanced_filters_show": "",
|
||||
"advanced_filters_hide": "",
|
||||
"advanced_filters_filters": "",
|
||||
"advanced_filters_sorters": "",
|
||||
"advanced_filters_filter_field": "",
|
||||
"advanced_filters_sorter_field": "",
|
||||
"advanced_filters_sorter_direction": "",
|
||||
"advanced_filters_filter_operator": "",
|
||||
"advanced_filters_filter_value": "",
|
||||
"dates": "",
|
||||
"employee": "",
|
||||
"filterson": "",
|
||||
|
||||
@@ -20,6 +20,8 @@ const AuditTrailMapping = {
|
||||
i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
|
||||
appointmentinsert: (start) =>
|
||||
i18n.t("audit_trail.messages.appointmentinsert", { start }),
|
||||
billdeleted: (invoice_number) =>
|
||||
i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
|
||||
billposted: (invoice_number) =>
|
||||
i18n.t("audit_trail.messages.billposted", { invoice_number }),
|
||||
billupdated: (invoice_number) =>
|
||||
@@ -33,6 +35,7 @@ const AuditTrailMapping = {
|
||||
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
||||
jobconverted: (ro_number) =>
|
||||
i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
||||
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
||||
jobfieldchange: (field, value) =>
|
||||
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
|
||||
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
|
||||
@@ -51,6 +54,8 @@ const AuditTrailMapping = {
|
||||
jobstatuschange: (status) =>
|
||||
i18n.t("audit_trail.messages.jobstatuschange", { status }),
|
||||
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
|
||||
jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }),
|
||||
jobvoid: () => i18n.t("audit_trail.messages.jobvoid"),
|
||||
};
|
||||
|
||||
export default AuditTrailMapping;
|
||||
|
||||
@@ -24,4 +24,13 @@ const range = {
|
||||
],
|
||||
"Last 90 Days": [moment().add(-90, "days"), moment()],
|
||||
};
|
||||
|
||||
// We are development, lets get crazy
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
range["Last year"] = [
|
||||
moment().subtract(1, "year"),
|
||||
moment(),
|
||||
];
|
||||
}
|
||||
|
||||
export default range;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { gql } from "@apollo/client";
|
||||
import {gql} from "@apollo/client";
|
||||
import jsreport from "@jsreport/browser-client";
|
||||
import { notification } from "antd";
|
||||
import {notification} from "antd";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
import { setEmailOptions } from "../redux/email/email.actions";
|
||||
import { store } from "../redux/store";
|
||||
import {auth} from "../firebase/firebase.utils";
|
||||
import {setEmailOptions} from "../redux/email/email.actions";
|
||||
import {store} from "../redux/store";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import cleanAxios from "./CleanAxios";
|
||||
import { TemplateList } from "./TemplateConstants";
|
||||
import {TemplateList} from "./TemplateConstants";
|
||||
import {generateTemplate} from "./graphQLmodifier";
|
||||
|
||||
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
|
||||
|
||||
jsreport.serverUrl = server;
|
||||
@@ -16,11 +18,11 @@ jsreport.serverUrl = server;
|
||||
const Templates = TemplateList();
|
||||
|
||||
export default async function RenderTemplate(
|
||||
templateObject,
|
||||
bodyshop,
|
||||
renderAsHtml = false,
|
||||
renderAsExcel = false,
|
||||
renderAsText = false
|
||||
templateObject,
|
||||
bodyshop,
|
||||
renderAsHtml = false,
|
||||
renderAsExcel = false,
|
||||
renderAsText = false
|
||||
) {
|
||||
if (window.jsr3) {
|
||||
jsreport.serverUrl = "https://reports3.test.imex.online/";
|
||||
@@ -30,41 +32,41 @@ export default async function RenderTemplate(
|
||||
jsreport.headers["Authorization"] = jsrAuth;
|
||||
|
||||
//Query assets that match the template name. Must be in format <<templateName>>.query
|
||||
let { contextData, useShopSpecificTemplate } = await fetchContextData(
|
||||
templateObject,
|
||||
jsrAuth
|
||||
let {contextData, useShopSpecificTemplate} = await fetchContextData(
|
||||
templateObject,
|
||||
jsrAuth
|
||||
);
|
||||
|
||||
const { ignoreCustomMargins } = Templates[templateObject.name];
|
||||
const {ignoreCustomMargins} = Templates[templateObject.name];
|
||||
|
||||
let reportRequest = {
|
||||
template: {
|
||||
name: useShopSpecificTemplate
|
||||
? `/${bodyshop.imexshopid}/${templateObject.name}`
|
||||
: `/${templateObject.name}`,
|
||||
? `/${bodyshop.imexshopid}/${templateObject.name}`
|
||||
: `/${templateObject.name}`,
|
||||
...(renderAsHtml
|
||||
? {}
|
||||
: {
|
||||
? {}
|
||||
: {
|
||||
recipe: "chrome-pdf",
|
||||
...(!ignoreCustomMargins && {
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
|
||||
...(renderAsText ? { recipe: "text" } : {}),
|
||||
...(renderAsExcel ? {recipe: "html-to-xlsx"} : {}),
|
||||
...(renderAsText ? {recipe: "text"} : {}),
|
||||
},
|
||||
data: {
|
||||
...contextData,
|
||||
@@ -73,7 +75,10 @@ export default async function RenderTemplate(
|
||||
headerpath: `/${bodyshop.imexshopid}/header.html`,
|
||||
footerpath: `/${bodyshop.imexshopid}/footer.html`,
|
||||
bodyshop: bodyshop,
|
||||
offset: bodyshop.timezone, //moment().utcOffset(),
|
||||
filters: templateObject?.filters,
|
||||
sorters: templateObject?.sorters,
|
||||
offset: bodyshop.timezone, //dayjs().utcOffset(),
|
||||
defaultSorters: templateObject?.defaultSorters,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -82,8 +87,8 @@ export default async function RenderTemplate(
|
||||
|
||||
if (!renderAsHtml) {
|
||||
render.download(
|
||||
(Templates[templateObject.name] &&
|
||||
Templates[templateObject.name].title) ||
|
||||
(Templates[templateObject.name] &&
|
||||
Templates[templateObject.name].title) ||
|
||||
""
|
||||
);
|
||||
} else {
|
||||
@@ -97,17 +102,17 @@ export default async function RenderTemplate(
|
||||
...(!ignoreCustomMargins && {
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -121,21 +126,21 @@ export default async function RenderTemplate(
|
||||
resolve({
|
||||
pdf,
|
||||
filename:
|
||||
Templates[templateObject.name] &&
|
||||
Templates[templateObject.name].title,
|
||||
Templates[templateObject.name] &&
|
||||
Templates[templateObject.name].title,
|
||||
html,
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification["error"]({ message: JSON.stringify(error) });
|
||||
notification["error"]({message: JSON.stringify(error)});
|
||||
}
|
||||
}
|
||||
|
||||
export async function RenderTemplates(
|
||||
templateObjects,
|
||||
bodyshop,
|
||||
renderAsHtml = false
|
||||
templateObjects,
|
||||
bodyshop,
|
||||
renderAsHtml = false
|
||||
) {
|
||||
//Query assets that match the template name. Must be in format <<templateName>>.query
|
||||
let unsortedTemplatesAndData = [];
|
||||
@@ -145,17 +150,17 @@ export async function RenderTemplates(
|
||||
|
||||
templateObjects.forEach((template) => {
|
||||
proms.push(
|
||||
(async () => {
|
||||
let { contextData, useShopSpecificTemplate } = await fetchContextData(
|
||||
template,
|
||||
jsrAuth
|
||||
);
|
||||
unsortedTemplatesAndData.push({
|
||||
templateObject: template,
|
||||
contextData,
|
||||
useShopSpecificTemplate,
|
||||
});
|
||||
})()
|
||||
(async () => {
|
||||
let {contextData, useShopSpecificTemplate} = await fetchContextData(
|
||||
template,
|
||||
jsrAuth
|
||||
);
|
||||
unsortedTemplatesAndData.push({
|
||||
templateObject: template,
|
||||
contextData,
|
||||
useShopSpecificTemplate,
|
||||
});
|
||||
})()
|
||||
);
|
||||
});
|
||||
await Promise.all(proms);
|
||||
@@ -172,8 +177,8 @@ export async function RenderTemplates(
|
||||
|
||||
unsortedTemplatesAndData.sort(function (a, b) {
|
||||
return (
|
||||
templateObjects.findIndex((x) => x.name === a.templateObject.name) -
|
||||
templateObjects.findIndex((x) => x.name === b.templateObject.name)
|
||||
templateObjects.findIndex((x) => x.name === a.templateObject.name) -
|
||||
templateObjects.findIndex((x) => x.name === b.templateObject.name)
|
||||
);
|
||||
});
|
||||
const templateAndData = unsortedTemplatesAndData;
|
||||
@@ -183,25 +188,25 @@ export async function RenderTemplates(
|
||||
let reportRequest = {
|
||||
template: {
|
||||
name: rootTemplate.useShopSpecificTemplate
|
||||
? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
|
||||
: `/${rootTemplate.templateObject.name}`,
|
||||
? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
|
||||
: `/${rootTemplate.templateObject.name}`,
|
||||
...(renderAsHtml
|
||||
? {}
|
||||
: {
|
||||
? {}
|
||||
: {
|
||||
recipe: "chrome-pdf",
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
},
|
||||
}),
|
||||
pdfOperations: [
|
||||
@@ -218,22 +223,22 @@ export async function RenderTemplates(
|
||||
template: {
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px",
|
||||
},
|
||||
name: template.useShopSpecificTemplate
|
||||
? `/${bodyshop.imexshopid}/${template.templateObject.name}`
|
||||
: `/${template.templateObject.name}`,
|
||||
...(renderAsHtml ? {} : { recipe: "chrome-pdf" }),
|
||||
? `/${bodyshop.imexshopid}/${template.templateObject.name}`
|
||||
: `/${template.templateObject.name}`,
|
||||
...(renderAsHtml ? {} : {recipe: "chrome-pdf"}),
|
||||
},
|
||||
type: "append",
|
||||
|
||||
@@ -245,8 +250,8 @@ export async function RenderTemplates(
|
||||
},
|
||||
data: {
|
||||
...extend(
|
||||
rootTemplate.contextData,
|
||||
...templateAndData.map((temp) => temp.contextData)
|
||||
rootTemplate.contextData,
|
||||
...templateAndData.map((temp) => temp.contextData)
|
||||
),
|
||||
|
||||
// ...rootTemplate.templateObject.variables,
|
||||
@@ -266,32 +271,33 @@ export async function RenderTemplates(
|
||||
return render.toString();
|
||||
}
|
||||
} catch (error) {
|
||||
notification["error"]({ message: JSON.stringify(error) });
|
||||
notification["error"]({message: JSON.stringify(error)});
|
||||
}
|
||||
}
|
||||
|
||||
export const GenerateDocument = async (
|
||||
template,
|
||||
messageOptions,
|
||||
sendType,
|
||||
jobid
|
||||
template,
|
||||
messageOptions,
|
||||
sendType,
|
||||
jobid
|
||||
) => {
|
||||
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
|
||||
if (sendType === "e") {
|
||||
store.dispatch(
|
||||
setEmailOptions({
|
||||
jobid,
|
||||
messageOptions: {
|
||||
...messageOptions,
|
||||
to: Array.isArray(messageOptions.to)
|
||||
? messageOptions.to
|
||||
: [messageOptions.to],
|
||||
},
|
||||
template,
|
||||
})
|
||||
setEmailOptions({
|
||||
jobid,
|
||||
messageOptions: {
|
||||
...messageOptions,
|
||||
to: Array.isArray(messageOptions.to)
|
||||
? messageOptions.to
|
||||
: [messageOptions.to],
|
||||
},
|
||||
template,
|
||||
})
|
||||
);
|
||||
} else if (sendType === "x") {
|
||||
console.log("excel");
|
||||
await RenderTemplate(template, bodyshop, false, true);
|
||||
} else if (sendType === "text") {
|
||||
await RenderTemplate(template, bodyshop, false, false, true);
|
||||
@@ -305,22 +311,74 @@ export const GenerateDocuments = async (templates) => {
|
||||
await RenderTemplates(templates, bodyshop);
|
||||
};
|
||||
|
||||
export const fetchFilterData = async ({name}) => {
|
||||
try {
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
const jsrAuth = (await axios.post("/utils/jsr")).data;
|
||||
jsreport.headers["FirebaseAuthorization"] =
|
||||
"Bearer " + (await auth.currentUser.getIdToken());
|
||||
|
||||
const folders = await cleanAxios.get(`${server}/odata/folders`, {
|
||||
headers: {Authorization: jsrAuth},
|
||||
});
|
||||
const shopSpecificFolder = folders.data.value.find(
|
||||
(f) => f.name === bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const jsReportFilters = await cleanAxios.get(
|
||||
`${server}/odata/assets?$filter=name eq '${name}.filters'`,
|
||||
{headers: {Authorization: jsrAuth}}
|
||||
);
|
||||
console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
|
||||
|
||||
let parsedFilterData;
|
||||
let useShopSpecificTemplate = false;
|
||||
// let shopSpecificTemplate;
|
||||
|
||||
if (shopSpecificFolder) {
|
||||
let shopSpecificTemplate = jsReportFilters.data.value.find(
|
||||
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||
);
|
||||
if (shopSpecificTemplate) {
|
||||
useShopSpecificTemplate = true;
|
||||
parsedFilterData = atob(shopSpecificTemplate.content);
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedFilterData) {
|
||||
const generalTemplate = jsReportFilters.data.value.find((f) => !f.folder);
|
||||
useShopSpecificTemplate = false;
|
||||
if (generalTemplate) parsedFilterData = atob(generalTemplate.content);
|
||||
}
|
||||
const data = JSON.parse(parsedFilterData);
|
||||
return {
|
||||
data,
|
||||
useShopSpecificTemplate,
|
||||
success: true,
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchContextData = async (templateObject, jsrAuth) => {
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
|
||||
jsreport.headers["FirebaseAuthorization"] =
|
||||
"Bearer " + (await auth.currentUser.getIdToken());
|
||||
"Bearer " + (await auth.currentUser.getIdToken());
|
||||
|
||||
const folders = await cleanAxios.get(`${server}/odata/folders`, {
|
||||
headers: { Authorization: jsrAuth },
|
||||
headers: {Authorization: jsrAuth},
|
||||
});
|
||||
const shopSpecificFolder = folders.data.value.find(
|
||||
(f) => f.name === bodyshop.imexshopid
|
||||
(f) => f.name === bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const jsReportQueries = await cleanAxios.get(
|
||||
`${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
|
||||
{ headers: { Authorization: jsrAuth } }
|
||||
`${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
|
||||
{headers: {Authorization: jsrAuth}}
|
||||
);
|
||||
|
||||
let templateQueryToExecute;
|
||||
@@ -329,7 +387,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
|
||||
|
||||
if (shopSpecificFolder) {
|
||||
let shopSpecificTemplate = jsReportQueries.data.value.find(
|
||||
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
|
||||
);
|
||||
if (shopSpecificTemplate) {
|
||||
useShopSpecificTemplate = true;
|
||||
@@ -343,16 +401,35 @@ const fetchContextData = async (templateObject, jsrAuth) => {
|
||||
templateQueryToExecute = atob(generalTemplate.content);
|
||||
}
|
||||
|
||||
let contextData = {};
|
||||
if (templateQueryToExecute) {
|
||||
const { data } = await client.query({
|
||||
query: gql(templateQueryToExecute),
|
||||
variables: { ...templateObject.variables },
|
||||
});
|
||||
contextData = data;
|
||||
// Commented out for future revision debugging
|
||||
// console.log('Template Object');
|
||||
// console.dir(templateObject);
|
||||
// console.log('Unmodified Query');
|
||||
// console.dir(templateQueryToExecute);
|
||||
|
||||
const hasFilters = templateObject?.filters?.length > 0;
|
||||
const hasSorters = templateObject?.sorters?.length > 0;
|
||||
const hasDefaultSorters = templateObject?.defaultSorters?.length > 0;
|
||||
|
||||
// We have no template filters or sorters, so we can just execute the query and return the data
|
||||
if (!hasFilters && !hasSorters && !hasDefaultSorters) {
|
||||
let contextData = {};
|
||||
if (templateQueryToExecute) {
|
||||
const {data} = await client.query({
|
||||
query: gql(templateQueryToExecute),
|
||||
variables: {...templateObject.variables},
|
||||
});
|
||||
contextData = data;
|
||||
}
|
||||
|
||||
return {contextData, useShopSpecificTemplate};
|
||||
}
|
||||
|
||||
return { contextData, useShopSpecificTemplate };
|
||||
return await generateTemplate(
|
||||
templateQueryToExecute,
|
||||
templateObject,
|
||||
useShopSpecificTemplate
|
||||
);
|
||||
};
|
||||
|
||||
//export const displayTemplateInWindow = (html) => {
|
||||
@@ -389,7 +466,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
|
||||
|
||||
function extend(o1, o2, o3) {
|
||||
var result = {},
|
||||
obj;
|
||||
obj;
|
||||
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
obj = arguments[i];
|
||||
@@ -405,4 +482,4 @@ function extend(o1, o2, o3) {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
373
client/src/utils/graphQLmodifier.js
Normal file
373
client/src/utils/graphQLmodifier.js
Normal file
@@ -0,0 +1,373 @@
|
||||
import {Kind, parse, print, visit} from "graphql";
|
||||
import client from "./GraphQLClient";
|
||||
import {gql} from "@apollo/client";
|
||||
|
||||
const STRING_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_like", label: "contains"},
|
||||
{value: "_nlike", label: "does not contain"},
|
||||
{value: "_ilike", label: "contains case-insensitive"},
|
||||
{value: "_nilike", label: "does not contain case-insensitive"}
|
||||
];
|
||||
const NUMBER_OPERATORS = [
|
||||
{value: "_eq", label: "equals"},
|
||||
{value: "_neq", label: "does not equal"},
|
||||
{value: "_gt", label: "greater than"},
|
||||
{value: "_lt", label: "less than"},
|
||||
{value: "_gte", label: "greater than or equal"},
|
||||
{value: "_lte", label: "less than or equal"}
|
||||
];
|
||||
const ORDER_BY_OPERATORS = [
|
||||
{value: "asc", label: "ascending"},
|
||||
{value: "desc", label: "descending"}
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the available operators for filtering
|
||||
* @returns {[{label: string, value: string},{label: string, value: string}]}
|
||||
*/
|
||||
export function getOrderOperatorsByType() {
|
||||
return ORDER_BY_OPERATORS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the available operators for filtering
|
||||
* @param type
|
||||
* @returns {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null]}
|
||||
*/
|
||||
export function getWhereOperatorsByType(type = 'string') {
|
||||
const operators = {
|
||||
string: STRING_OPERATORS,
|
||||
number: NUMBER_OPERATORS
|
||||
};
|
||||
return operators[type];
|
||||
}
|
||||
|
||||
/* eslint-disable no-loop-func */
|
||||
|
||||
/**
|
||||
* Parse a GraphQL query into an AST
|
||||
* @param query
|
||||
* @returns {DocumentNode}
|
||||
*/
|
||||
export function parseQuery(query) {
|
||||
return parse(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an AST back into a GraphQL query
|
||||
* @param query
|
||||
* @returns {string}
|
||||
*/
|
||||
export function printQuery(query) {
|
||||
return print(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a template based on the query and object
|
||||
* @param templateQueryToExecute
|
||||
* @param templateObject
|
||||
* @param useShopSpecificTemplate
|
||||
* @returns {Promise<{contextData: {}, useShopSpecificTemplate}>}
|
||||
*/
|
||||
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) {
|
||||
// Advanced Filtering and Sorting modifications start here
|
||||
|
||||
// Parse the query and apply the filters and sorters
|
||||
const ast = parseQuery(templateQueryToExecute);
|
||||
|
||||
let filterFields = [];
|
||||
|
||||
if (templateObject?.filters && templateObject?.filters?.length) {
|
||||
applyFilters(ast, templateObject.filters, filterFields);
|
||||
wrapFiltersInAnd(ast, filterFields);
|
||||
}
|
||||
|
||||
if (templateObject?.sorters && templateObject?.sorters?.length) {
|
||||
applySorters(ast, templateObject.sorters);
|
||||
} else if (templateObject?.defaultSorters && templateObject?.defaultSorters?.length) {
|
||||
applySorters(ast, templateObject.defaultSorters);
|
||||
}
|
||||
|
||||
const finalQuery = printQuery(ast);
|
||||
|
||||
// commented out for future revision debugging
|
||||
// console.log('Modified Query');
|
||||
// console.log(finalQuery);
|
||||
|
||||
let contextData = {};
|
||||
if (templateQueryToExecute) {
|
||||
const {data} = await client.query({
|
||||
query: gql(finalQuery),
|
||||
variables: {...templateObject.variables},
|
||||
});
|
||||
contextData = data;
|
||||
}
|
||||
|
||||
return {contextData, useShopSpecificTemplate};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Apply sorters to the AST
|
||||
* @param ast
|
||||
* @param sorters
|
||||
*/
|
||||
export function applySorters(ast, sorters) {
|
||||
sorters.forEach((sorter) => {
|
||||
const fieldPath = sorter.field.split('.');
|
||||
visit(ast, {
|
||||
OperationDefinition: {
|
||||
enter(node) {
|
||||
// Loop through each sorter to apply it
|
||||
// noinspection DuplicatedCode
|
||||
|
||||
let currentSelection = node; // Start with the root operation
|
||||
|
||||
// Navigate down the field path to the correct location
|
||||
for (let i = 0; i < fieldPath.length - 1; i++) {
|
||||
let found = false;
|
||||
visit(currentSelection, {
|
||||
Field: {
|
||||
enter(node) {
|
||||
if (node.name.value === fieldPath[i]) {
|
||||
currentSelection = node; // Move down to the next level
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!found) break; // Stop if we can't find the next field in the path
|
||||
}
|
||||
|
||||
// Apply the sorter at the correct level
|
||||
if (currentSelection) {
|
||||
const targetFieldName = fieldPath[fieldPath.length - 1];
|
||||
let orderByArg = currentSelection.arguments.find(arg => arg.name.value === 'order_by');
|
||||
if (!orderByArg) {
|
||||
orderByArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: { kind: Kind.NAME, value: 'order_by' },
|
||||
value: { kind: Kind.OBJECT, fields: [] },
|
||||
};
|
||||
currentSelection.arguments.push(orderByArg);
|
||||
}
|
||||
|
||||
const sorterField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: targetFieldName },
|
||||
value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions
|
||||
};
|
||||
|
||||
// Add the new sorter condition
|
||||
orderByArg.value.fields.push(sorterField);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to the AST
|
||||
* @param ast
|
||||
* @param filters
|
||||
*/
|
||||
export function applyFilters(ast, filters) {
|
||||
return visit(ast, {
|
||||
OperationDefinition: {
|
||||
enter(node) {
|
||||
filters.forEach(filter => {
|
||||
const fieldPath = filter.field.split('.');
|
||||
let topLevel = false;
|
||||
|
||||
// Determine if the filter should be applied at the top level
|
||||
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
|
||||
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
|
||||
topLevel = true;
|
||||
}
|
||||
|
||||
if (topLevel) {
|
||||
// Construct the filter for a top-level application
|
||||
const targetFieldName = fieldPath[fieldPath.length - 1];
|
||||
const filterValue = {
|
||||
kind: getGraphQLKind(filter.value),
|
||||
value: filter.value,
|
||||
};
|
||||
|
||||
const nestedFilter = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: targetFieldName },
|
||||
value: {
|
||||
kind: Kind.OBJECT,
|
||||
fields: [{
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: filter.operator },
|
||||
value: filterValue,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
// Find or create the where argument for the top-level field
|
||||
let whereArg = node.selectionSet.selections
|
||||
.find(selection => selection.name.value === fieldPath[0])
|
||||
?.arguments.find(arg => arg.name.value === 'where');
|
||||
|
||||
if (!whereArg) {
|
||||
whereArg = {
|
||||
kind: Kind.ARGUMENT,
|
||||
name: { kind: Kind.NAME, value: 'where' },
|
||||
value: { kind: Kind.OBJECT, fields: [] },
|
||||
};
|
||||
const topLevelSelection = node.selectionSet.selections.find(selection =>
|
||||
selection.name.value === fieldPath[0]
|
||||
);
|
||||
if (topLevelSelection) {
|
||||
topLevelSelection.arguments = topLevelSelection.arguments || [];
|
||||
topLevelSelection.arguments.push(whereArg);
|
||||
}
|
||||
}
|
||||
|
||||
// Correctly position the nested filter without an extra 'where'
|
||||
if (fieldPath.length > 2) { // More than one level deep
|
||||
let currentField = whereArg.value;
|
||||
fieldPath.slice(1, -1).forEach((path, index) => {
|
||||
let existingField = currentField.fields.find(f => f.name.value === path);
|
||||
if (!existingField) {
|
||||
existingField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: path },
|
||||
value: { kind: Kind.OBJECT, fields: [] }
|
||||
};
|
||||
currentField.fields.push(existingField);
|
||||
}
|
||||
currentField = existingField.value;
|
||||
});
|
||||
currentField.fields.push(nestedFilter);
|
||||
} else { // Directly under the top level
|
||||
whereArg.value.fields.push(nestedFilter);
|
||||
}
|
||||
} else {
|
||||
// Initialize a reference to the current selection to traverse down the AST
|
||||
let currentSelection = node;
|
||||
let whereArgFound = false;
|
||||
|
||||
// Iterate over the fieldPath, except for the last entry, to navigate the structure
|
||||
for (let i = 0; i < fieldPath.length - 1; i++) {
|
||||
const fieldName = fieldPath[i];
|
||||
let fieldFound = false;
|
||||
|
||||
// Check if the current selection has a selectionSet and selections
|
||||
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
|
||||
// Look for the field in the current selection's selections
|
||||
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
|
||||
if (selection) {
|
||||
// Move down the AST to the found selection
|
||||
currentSelection = selection;
|
||||
fieldFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the field was not found in the current path, it's an issue
|
||||
if (!fieldFound) {
|
||||
console.error(`Field ${fieldName} not found in the current selection.`);
|
||||
return; // Exit the loop and function due to error
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, currentSelection should be the parent field where the filter needs to be applied
|
||||
// Check if the 'where' argument already exists in the current selection
|
||||
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
|
||||
if (whereArg) {
|
||||
whereArgFound = true;
|
||||
} else {
|
||||
// If not found, create a new 'where' argument for the current selection
|
||||
currentSelection.arguments.push({
|
||||
kind: Kind.ARGUMENT,
|
||||
name: { kind: Kind.NAME, value: 'where' },
|
||||
value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter
|
||||
});
|
||||
}
|
||||
|
||||
// Assuming the last entry in fieldPath is the field to apply the filter on
|
||||
const targetField = fieldPath[fieldPath.length - 1];
|
||||
const filterValue = {
|
||||
kind: getGraphQLKind(filter.value),
|
||||
value: filter.value,
|
||||
};
|
||||
|
||||
// Construct the filter field object
|
||||
const filterField = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: targetField },
|
||||
value: {
|
||||
kind: Kind.OBJECT,
|
||||
fields: [{
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: { kind: Kind.NAME, value: filter.operator },
|
||||
value: filterValue,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
// Add the filter field to the 'where' clause of the current selection
|
||||
if (whereArgFound) {
|
||||
whereArg.value.fields.push(filterField);
|
||||
} else {
|
||||
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
|
||||
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GraphQL kind for a value
|
||||
* @param value
|
||||
* @returns {Kind|Kind.INT}
|
||||
*/
|
||||
function getGraphQLKind(value) {
|
||||
if (typeof value === 'number') {
|
||||
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return Kind.BOOLEAN;
|
||||
} else if (typeof value === 'string') {
|
||||
return Kind.STRING;
|
||||
}
|
||||
// Extend with more types as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap filters in an 'and' object
|
||||
* @param ast
|
||||
* @param filterFields
|
||||
*/
|
||||
export function wrapFiltersInAnd(ast, filterFields) {
|
||||
visit(ast, {
|
||||
OperationDefinition: {
|
||||
enter(node) {
|
||||
node.selectionSet.selections.forEach((selection) => {
|
||||
let whereArg = selection.arguments.find(arg => arg.name.value === 'where');
|
||||
if (filterFields.length > 1) {
|
||||
const andFilter = {
|
||||
kind: Kind.OBJECT_FIELD,
|
||||
name: {kind: Kind.NAME, value: '_and'},
|
||||
value: {kind: Kind.LIST, values: filterFields}
|
||||
};
|
||||
whereArg.value.fields.push(andFilter);
|
||||
} else if (filterFields.length === 1) {
|
||||
whereArg.value.fields.push(filterFields[0].fields[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable no-loop-func */
|
||||
@@ -74,6 +74,7 @@ app.use('/adm', require("./server/routes/adminRoutes"));
|
||||
app.use('/tech', require("./server/routes/techRoutes"));
|
||||
app.use('/intellipay', require("./server/routes/intellipayRoutes"));
|
||||
app.use('/cdk', require("./server/routes/cdkRoutes"));
|
||||
app.use('/csi', require("./server/routes/csiRoutes"));
|
||||
|
||||
// Default route for forbidden access
|
||||
app.get("/", (req, res) => {
|
||||
|
||||
2
server/csi/csi.js
Normal file
2
server/csi/csi.js
Normal file
@@ -0,0 +1,2 @@
|
||||
exports.lookup = require("./lookup").default;
|
||||
exports.submit = require("./submit").default;
|
||||
24
server/csi/lookup.js
Normal file
24
server/csi/lookup.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const path = require("path");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(
|
||||
process.cwd(),
|
||||
`.env.${process.env.NODE_ENV || "development"}`
|
||||
),
|
||||
});
|
||||
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
try {
|
||||
logger.log("csi-surveyID-lookup", "DEBUG", "csi", req.body.surveyId, null);
|
||||
const gql_response = await client.request(queries.QUERY_SURVEY, {
|
||||
surveyId: req.body.surveyId,
|
||||
});
|
||||
res.status(200).json(gql_response);
|
||||
} catch (error) {
|
||||
logger.log("csi-surveyID-lookup", "ERROR", "csi", req.body.surveyId, error);
|
||||
res.status(400).json(error);
|
||||
}
|
||||
};
|
||||
29
server/csi/submit.js
Normal file
29
server/csi/submit.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const path = require("path");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(
|
||||
process.cwd(),
|
||||
`.env.${process.env.NODE_ENV || "development"}`
|
||||
),
|
||||
});
|
||||
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
try {
|
||||
logger.log("csi-surveyID-submit", "DEBUG", "csi", req.body.surveyId, null);
|
||||
const gql_response = await client.request(queries.COMPLETE_SURVEY, {
|
||||
surveyId: req.body.surveyId,
|
||||
survey: {
|
||||
response: req.body.values,
|
||||
valid: false,
|
||||
completedon: new Date(),
|
||||
},
|
||||
});
|
||||
res.status(200).json(gql_response);
|
||||
} catch (error) {
|
||||
logger.log("csi-surveyID-submit", "ERROR", "csi", req.body.surveyId, error);
|
||||
res.status(400).json(error);
|
||||
}
|
||||
};
|
||||
@@ -2156,3 +2156,23 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
|
||||
shopid
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.QUERY_SURVEY = `query QUERY_SURVEY($surveyId: uuid!) {
|
||||
csi_by_pk(id: $surveyId) {
|
||||
completedon
|
||||
csiquestion {
|
||||
id
|
||||
config
|
||||
}
|
||||
id
|
||||
relateddata
|
||||
valid
|
||||
validuntil
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.COMPLETE_SURVEY = `mutation COMPLETE_SURVEY($surveyId: uuid!, $survey: csi_set_input) {
|
||||
update_csi(where: { id: { _eq: $surveyId } }, _set: $survey) {
|
||||
affected_rows
|
||||
}
|
||||
}`;
|
||||
8
server/routes/csiRoutes.js
Normal file
8
server/routes/csiRoutes.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { lookup, submit } = require("../csi/csi");
|
||||
|
||||
router.post("/lookup", lookup);
|
||||
router.post("/submit", submit);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user