Merged in release/2024-02-23 (pull request #1307)

Release/2024 02 23
This commit is contained in:
Dave Richer
2024-02-23 22:00:15 +00:00
20 changed files with 1384 additions and 427 deletions

View File

@@ -3,6 +3,9 @@
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
@@ -36,6 +39,35 @@ const schema = {
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",`
@@ -71,7 +103,6 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
}
```
### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query.
@@ -114,7 +145,23 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
- 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"
}
}
```

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}
}
}
`;
}`;
};

View File

@@ -128,7 +128,7 @@
height: 100%;
width: 100%;
.ant-card-body {
height: 80%;
height: calc(100% - 2rem);
width: 100%;
// // background-color: red;
// height: 90%;

View File

@@ -123,11 +123,16 @@ 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>
)}

View File

@@ -1,52 +1,352 @@
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd";
import React, {useEffect, useState} from "react";
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 {getOperatorsByType} from "../../utils/graphQLmodifier";
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}) {
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}/>;
return <RenderFilters form={form} templateId={key} bodyshop={bodyshop}/>;
}}
</Form.Item>
);
}
function RenderFilters({templateId, form}) {
/**
* 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();
useEffect(() => {
const fetch = async () => {
setIsLoading(true);
const data = await fetchFilterData({name: templateId});
if (data?.success) {
setState(data.data);
} else {
setState(null);
}
setIsLoading(false);
};
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]);
}, [templateId, fetch]);
const filters = useMemo(() => state?.filters || [], [state]);
const sorters = useMemo(() => state?.sorters || [], [state]);
// Conditional display of filters and sorters
if (!templateId) return null;
if (isLoading) return <LoadingSkeleton/>;
if (!state) return null;
// Filters and Sorters data available
return (
<div style={{marginTop: '10px'}}>
<Checkbox
@@ -56,215 +356,11 @@ function RenderFilters({templateId, form}) {
/>
{visible && (
<div>
{state.filters && state.filters.length > 0 && (
<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="field"
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={
state.filters
? state.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 = state.filters.find(f => f.name === name)?.type;
return <Form.Item
key={`${index}operator`}
label="operator"
name={[field.name, "operator"]}
dependencies={[]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select options={getOperatorsByType(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 = state.filters.find(f => f.name === name)?.type;
return <Form.Item
key={`${index}value`}
label="value"
name={[field.name, "value"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
{type === 'number' ?
<InputNumber
onChange={(value) => {
form.setFieldsValue({[field.name]: {value: parseInt(value)}});
}}
/>
:
<Input
onChange={(value) => {
form.setFieldsValue({[field.name]: {value: value.toString()}});
}}
/>
}
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem"}}
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>
{filters.length > 0 && (
<FiltersSection filters={filters} form={form} bodyshop={bodyshop}/>
)}
{state.sorters && state.sorters.length > 0 && (
<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="field"
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={
state.sorters
? state.sorters.map((f) => ({
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}))
: []
}
/>
</Form.Item>
</Col>
<Col span={11}>
<Form.Item
key={`${index}direction`}
label="direction"
name={[field.name, "direction"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={[
{value: "desc", label: "Descending"},
{value: "asc", label: "Ascending"},
]}
/>
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem"}}
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.length > 0 && (
<SortersSection sorters={sorters} form={form}/>
)}
</div>
)}

View File

@@ -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,}

View File

@@ -16,9 +16,11 @@ import EmployeeSearchSelect from "../employee-search-select/employee-search-sele
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))
@@ -28,7 +30,7 @@ export default connect(
mapDispatchToProps
)(ReportCenterModalComponent);
export function ReportCenterModalComponent({reportCenterModal}) {
export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
const [form] = Form.useForm();
const [search, setSearch] = useState("");
@@ -64,22 +66,28 @@ export function ReportCenterModalComponent({reportCenterModal}) {
const end = values.dates ? values.dates[1] : null;
const { id } = values;
await GenerateDocument(
{
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") } : {}),
...(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,
@@ -117,7 +125,8 @@ export function ReportCenterModalComponent({reportCenterModal}) {
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
<Form.Item
<Form.Item name="defaultSorters" hidden/>
<Form.Item
name="key"
label={t("reportcenter.labels.key")}
// className="radio-group-columns"
@@ -181,7 +190,7 @@ export function ReportCenterModalComponent({reportCenterModal}) {
);
}}
</Form.Item>
<ReportCenterModalFiltersSortersComponent form={form} />
<ReportCenterModalFiltersSortersComponent form={form} bodyshop={bodyshop} />
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
@@ -236,6 +245,9 @@ export function ReportCenterModalComponent({reportCenterModal}) {
{() => {
const key = form.getFieldValue("key");
const datedisable = Templates[key] && Templates[key].datedisable;
// TODO: MERGE NOTE, Ranges turns to presets in DatePicker.RangePicker
if (datedisable !== true) {
return (
<Form.Item
@@ -250,7 +262,7 @@ export function ReportCenterModalComponent({reportCenterModal}) {
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
presets={DatePickerRanges}
ranges={DatePickerRanges}
/>
</Form.Item>
);

View File

@@ -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>
);

View File

@@ -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,

View File

@@ -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

View File

@@ -747,6 +747,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",
@@ -874,6 +875,7 @@
"labels": {
"bodyhrs": "Body Hrs",
"dollarsinproduction": "Dollars in Production",
"phone": "Phone",
"prodhrs": "Production Hrs",
"refhrs": "Refinish Hrs"
},
@@ -889,8 +891,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": {
@@ -2579,6 +2583,11 @@
"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}}",

View File

@@ -747,6 +747,7 @@
"driverinformation": "",
"findcontract": "",
"findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "",
"populatefromjob": "",
"rates": "",
@@ -874,6 +875,7 @@
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
"phone": "",
"prodhrs": "",
"refhrs": ""
},
@@ -889,7 +891,9 @@
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "",
"scheduledoutdate": "",
"scheduledouttoday": ""
}
},
@@ -2579,6 +2583,11 @@
"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": "",

View File

@@ -747,6 +747,7 @@
"driverinformation": "",
"findcontract": "",
"findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "",
"populatefromjob": "",
"rates": "",
@@ -874,6 +875,7 @@
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
"phone": "",
"prodhrs": "",
"refhrs": ""
},
@@ -889,7 +891,9 @@
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "",
"scheduledoutdate": "",
"scheduledouttoday": ""
}
},
@@ -2579,6 +2583,11 @@
"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": "",

View File

@@ -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;

View File

@@ -9,7 +9,7 @@ import {store} from "../redux/store";
import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios";
import {TemplateList} from "./TemplateConstants";
import {applyFilters, applySorters, parseQuery, printQuery, wrapFiltersInAnd} from "./graphQLmodifier";
import {generateTemplate} from "./graphQLmodifier";
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
@@ -75,7 +75,10 @@ export default async function RenderTemplate(
headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop,
filters: templateObject?.filters,
sorters: templateObject?.sorters,
offset: bodyshop.timezone, //dayjs().utcOffset(),
defaultSorters: templateObject?.defaultSorters,
},
};
@@ -278,7 +281,9 @@ export const GenerateDocument = async (
sendType,
jobid
) => {
const bodyshop = store.getState().user.bodyshop;
if (sendType === "e") {
store.dispatch(
setEmailOptions({
@@ -402,9 +407,12 @@ const fetchContextData = async (templateObject, jsrAuth) => {
// 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 ((!templateObject?.filters && !templateObject?.filters?.length && !templateObject?.sorters && !templateObject?.sorters?.length)) {
if (!hasFilters && !hasSorters && !hasDefaultSorters) {
let contextData = {};
if (templateQueryToExecute) {
const {data} = await client.query({
@@ -417,36 +425,11 @@ const fetchContextData = async (templateObject, jsrAuth) => {
return {contextData, useShopSpecificTemplate};
}
// 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);
}
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};
return await generateTemplate(
templateQueryToExecute,
templateObject,
useShopSpecificTemplate
);
};
//export const displayTemplateInWindow = (html) => {

View File

@@ -1,4 +1,6 @@
import {Kind, parse, print, visit} from "graphql";
import client from "./GraphQLClient";
import {gql} from "@apollo/client";
const STRING_OPERATORS = [
{value: "_eq", label: "equals"},
@@ -16,8 +18,26 @@ const NUMBER_OPERATORS = [
{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"}
];
export function getOperatorsByType(type = 'string') {
/**
* 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
@@ -44,6 +64,52 @@ export function parseQuery(query) {
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
@@ -261,8 +327,6 @@ export function applyFilters(ast, filters) {
});
}
/**
* Get the GraphQL kind for a value
* @param value