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

Release/2024 02 23
This commit is contained in:
Allan Carr
2024-02-22 21:53:33 +00:00
20 changed files with 1375 additions and 424 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 This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports. modify the graphQL query and provide the user more power over their reports.
# Special Notes
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
## High level Schema Overview ## High level Schema Overview
```javascript ```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. 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. 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 ### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",` `"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 ### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",` `"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query. This will produce a where clause at the `jobs` level of the graphQL query.
@@ -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 things that are already filtered as part of the original query, this would be redundant and could cause issues.
- Do not add the ability to filter on things like FK constraints, must like the above example. - Do not add the ability to filter on things like FK constraints, must like the above example.
## Sorters ## Sorters
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level. - 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. - 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 /> <FormDateTimePicker />
</Form.Item> </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>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item <Form.Item
@@ -90,16 +114,17 @@ export default function ContractFormComponent({
> >
{() => { {() => {
const mileageOver = const mileageOver =
selectedCar && selectedCar && selectedCar.nextservicekm
selectedCar.nextservicekm <= form.getFieldValue("kmstart"); ? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
: false;
const dueForService = const dueForService =
selectedCar && selectedCar &&
selectedCar.nextservicedate && selectedCar.nextservicedate &&
moment(selectedCar.nextservicedate).isBefore( moment(selectedCar.nextservicedate)
moment(form.getFieldValue("scheduledreturn")) .endOf("day")
); .isSameOrBefore(
moment(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService) if (mileageOver || dueForService)
return ( return (
<Space direction="vertical" style={{ color: "tomato" }}> <Space direction="vertical" style={{ color: "tomato" }}>
@@ -117,7 +142,6 @@ export default function ContractFormComponent({
</span> </span>
</Space> </Space>
); );
return <></>; return <></>;
}} }}
</Form.Item> </Form.Item>

View File

@@ -72,7 +72,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
const { nextservicedate, nextservicekm, mileage } = record; const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
@@ -80,11 +81,25 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
nextservicedate && nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment()); moment(nextservicedate).endOf("day").isSameOrBefore(moment());
const insuranceOver =
insuranceexpires &&
moment(insuranceexpires).endOf("day").isBefore(moment());
return ( return (
<Space> <Space>
{t(record.status)} {t(record.status)}
{(mileageOver || dueForService) && ( {(mileageOver || dueForService || insuranceOver) && (
<Tooltip title={t("contracts.labels.cardueforservice")}> <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" }} /> <WarningFilled style={{ color: "tomato" }} />
</Tooltip> </Tooltip>
)} )}

View File

@@ -3,21 +3,31 @@ import {
ExclamationCircleFilled, ExclamationCircleFilled,
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } 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 moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { 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 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 DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledInToday({ data, ...cardProps }) { export default function DashboardScheduledInToday({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
}); });
const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage(
"isTvModeScheduledIn",
false
);
if (!data) return null; if (!data) return null;
if (!data.scheduled_in_today) if (!data.scheduled_in_today)
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
@@ -31,6 +41,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
alt_transport: item.job.alt_transport, alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no, clm_no: item.job.clm_no,
jobid: item.job.jobid, 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, ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent, iouparent: item.job.iouparent,
ownerid: item.job.ownerid, ownerid: item.job.ownerid,
@@ -49,7 +65,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
v_vin: item.job.v_vin, v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid, vehicleid: item.job.vehicleid,
note: item.note, note: item.note,
start: moment(item.start).format("hh:mm a"), start: item.start,
title: item.title, title: item.title,
}; };
appt.push(i); appt.push(i);
@@ -59,11 +75,191 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
return new moment(a.start) - new moment(b.start); 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"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
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))) ||
[],
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) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.jobid} to={"/manage/jobs/" + record.jobid}
@@ -91,7 +287,10 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
responsive: ["md"], sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link <Link
@@ -108,23 +307,16 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
}, },
}, },
{ {
title: t("jobs.fields.ownr_ph1"), title: t("dashboard.labels.phone"),
dataIndex: "ownr_ph1", dataIndex: "ownr_ph",
key: "ownr_ph1", key: "ownr_ph",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} /> <Space size="small" wrap>
), <ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
}, <ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
{ </Space>
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} />
), ),
}, },
{ {
@@ -134,7 +326,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} /> <a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
), ),
}, },
{ {
@@ -142,6 +334,15 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -165,43 +366,78 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
}, sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
{ sortOrder:
title: t("appointments.fields.time"), state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
dataIndex: "start", filters:
key: "start", (appt &&
ellipsis: true, appt
responsive: ["md"], .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"), title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport", dataIndex: "alt_transport",
key: "alt_transport", key: "alt_transport",
ellipsis: true, 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))) ||
[],
}, },
]; ];
const handleTableChange = (sorter) => { const handleTableChange = (sorter) => {
setState({ ...state, sortedInfo: sorter }); setState({ ...state, sortedInfo: sorter });
}; };
return ( return (
<Card <Card
title={t("dashboard.titles.scheduledintoday", { title={t("dashboard.titles.scheduledindate", {
date: moment().startOf("day").format("MM/DD/YYYY"), 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} {...cardProps}
> >
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={false}
columns={columns} columns={isTvModeScheduledIn ? tvColumns : columns}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}
dataSource={appt} dataSource={appt}
size={isTvModeScheduledIn ? "small" : "middle"}
/> />
</div> </div>
</Card> </Card>
@@ -220,6 +456,10 @@ export const DashboardScheduledInTodayGql = `
alt_transport alt_transport
clm_no clm_no
jobid: id jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm ins_co_nm
iouparent iouparent
ownerid ownerid

View File

@@ -3,37 +3,271 @@ import {
ExclamationCircleFilled, ExclamationCircleFilled,
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } 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 moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { 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 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 DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledOutToday({ data, ...cardProps }) { export default function DashboardScheduledOutToday({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
}); });
const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage(
"isTvModeScheduledOut",
false
);
if (!data) return null; if (!data) return null;
if (!data.scheduled_out_today) if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
data.scheduled_out_today.forEach((item) => { 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) { data.scheduled_out_today.sort(function (a, b) {
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion); 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"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
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))) ||
[],
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))) ||
[],
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) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.jobid} to={"/manage/jobs/" + record.jobid}
@@ -61,7 +295,10 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
responsive: ["md"], sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link <Link
@@ -78,23 +315,16 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
}, },
}, },
{ {
title: t("jobs.fields.ownr_ph1"), title: t("dashboard.labels.phone"),
dataIndex: "ownr_ph1", dataIndex: "ownr_ph",
key: "ownr_ph1", key: "ownr_ph",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} /> <Space size="small" wrap>
), <ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
}, <ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
{ </Space>
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} />
), ),
}, },
{ {
@@ -104,7 +334,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} /> <a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
), ),
}, },
{ {
@@ -112,6 +342,15 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -135,20 +374,46 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
}, sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
{ sortOrder:
title: t("jobs.fields.scheduled_completion"), state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
dataIndex: "scheduled_completion", filters:
key: "scheduled_completion", (data.scheduled_out_today &&
ellipsis: true, data.scheduled_out_today
responsive: ["md"], .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"), title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport", dataIndex: "alt_transport",
key: "alt_transport", key: "alt_transport",
ellipsis: true, 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))) ||
[],
}, },
]; ];
@@ -158,20 +423,30 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
return ( return (
<Card <Card
title={t("dashboard.titles.scheduledouttoday", { title={t("dashboard.titles.scheduledoutdate", {
date: moment().startOf("day").format("MM/DD/YYYY"), 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} {...cardProps}
> >
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={false}
columns={columns} columns={isTvModeScheduledOut ? tvColumns : columns}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}
dataSource={data.scheduled_out_today} dataSource={data.scheduled_out_today}
size={isTvModeScheduledOut ? "small" : "middle"}
/> />
</div> </div>
</Card> </Card>
@@ -188,6 +463,10 @@ export const DashboardScheduledOutTodayGql = `
alt_transport alt_transport
clm_no clm_no
jobid: id jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm ins_co_nm
iouparent iouparent
ownerid ownerid
@@ -200,6 +479,7 @@ export const DashboardScheduledOutTodayGql = `
production_vars production_vars
ro_number ro_number
scheduled_completion scheduled_completion
status
suspended suspended
v_make_desc v_make_desc
v_model_desc v_model_desc

View File

@@ -275,26 +275,22 @@ const componentList = {
h: 2, h: 2,
}, },
ScheduleInToday: { ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday", { label: i18next.t("dashboard.titles.scheduledintoday"),
date: moment().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledInToday, component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql, gqlFragment: DashboardScheduledInTodayGql,
minW: 10, minW: 6,
minH: 2, minH: 2,
w: 10, w: 10,
h: 2, h: 3,
}, },
ScheduleOutToday: { ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday", { label: i18next.t("dashboard.titles.scheduledouttoday"),
date: moment().startOf("day").format("MM/DD/YYYY"),
}),
component: DashboardScheduledOutToday, component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql, gqlFragment: DashboardScheduledOutTodayGql,
minW: 10, minW: 6,
minH: 2, minH: 2,
w: 10, w: 10,
h: 2, h: 3,
}, },
}; };
@@ -306,8 +302,7 @@ const createDashboardQuery = (state) => {
.map((item, index) => componentList[item.i].gqlFragment || "") .map((item, index) => componentList[item.i].gqlFragment || "")
.join(""); .join("");
return gql` return gql`
query QUERY_DASHBOARD_DETAILS { query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [ monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}}, { voided: {_eq: false}},
{date_invoiced: {_gte: "${moment() {date_invoiced: {_gte: "${moment()
@@ -317,11 +312,11 @@ const createDashboardQuery = (state) => {
.endOf("month") .endOf("month")
.endOf("day") .endOf("day")
.toISOString()}"}}]}) { .toISOString()}"}}]}) {
id id
ro_number ro_number
date_invoiced date_invoiced
job_totals job_totals
rate_la1 rate_la1
rate_la2 rate_la2
rate_la3 rate_la3
rate_la4 rate_la4
@@ -344,43 +339,42 @@ const createDashboardQuery = (state) => {
rate_mapa rate_mapa
rate_mash rate_mash
rate_matd rate_matd
joblines(where: { removed: { _eq: false } }) { joblines(where: { removed: { _eq: false } }) {
id id
mod_lbr_ty mod_lbr_ty
mod_lb_hrs mod_lb_hrs
act_price act_price
part_qty part_qty
part_type part_type
}
} }
} production_jobs: jobs(where: { inproduction: { _eq: true } }) {
production_jobs: jobs(where: { inproduction: { _eq: true } }) { id
ro_number
ins_co_nm
job_totals
joblines(where: { removed: { _eq: false } }) {
id id
ro_number mod_lbr_ty
ins_co_nm mod_lb_hrs
job_totals act_price
joblines(where: { removed: { _eq: false } }) { part_qty
id part_type
mod_lbr_ty }
mod_lb_hrs labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
act_price aggregate {
part_qty sum {
part_type mod_lb_hrs
}
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 { larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
sum { aggregate {
mod_lb_hrs sum {
} mod_lb_hrs
} }
} }
} }
} }
`; }`;
}; };

View File

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

View File

@@ -123,11 +123,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
</DataLabel> </DataLabel>
{job.cccontracts.length > 0 && ( {job.cccontracts.length > 0 && (
<DataLabel label={t("jobs.labels.contracts")}> <DataLabel label={t("jobs.labels.contracts")}>
{job.cccontracts.map((c) => ( {job.cccontracts.map((c, index) => (
<Link <Space wrap>
key={c.id} <Link
to={`/manage/courtesycars/contracts/${c.id}`} key={c.id}
>{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}</Link> 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>
)} )}

View File

@@ -1,52 +1,352 @@
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd"; 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 {fetchFilterData} from "../../utils/RenderTemplate";
import {DeleteFilled} from "@ant-design/icons"; import {DeleteFilled} from "@ant-design/icons";
import {useTranslation} from "react-i18next"; 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 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 ( return (
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}> <Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => { {() => {
const key = form.getFieldValue("key"); const key = form.getFieldValue("key");
return <RenderFilters form={form} templateId={key}/>; return <RenderFilters form={form} templateId={key} bodyshop={bodyshop}/>;
}} }}
</Form.Item> </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 [state, setState] = useState(null);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation(); const {t} = useTranslation();
useEffect(() => { const fetch = useCallback(async () => {
const fetch = async () => { // Reset all the filters and Sorters.
setIsLoading(true); form.resetFields(['filters']);
const data = await fetchFilterData({name: templateId}); form.resetFields(['sorters']);
if (data?.success) { form.resetFields(['defaultSorters']);
setState(data.data);
} else {
setState(null);
}
setIsLoading(false);
};
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) { if (templateId) {
fetch(); 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 (!templateId) return null;
if (isLoading) return <LoadingSkeleton/>; if (isLoading) return <LoadingSkeleton/>;
if (!state) return null; if (!state) return null;
// Filters and Sorters data available
return ( return (
<div style={{marginTop: '10px'}}> <div style={{marginTop: '10px'}}>
<Checkbox <Checkbox
@@ -56,215 +356,11 @@ function RenderFilters({templateId, form}) {
/> />
{visible && ( {visible && (
<div> <div>
{state.filters && state.filters.length > 0 && ( {filters.length > 0 && (
<Card type='inner' title={ t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}> <FiltersSection filters={filters} form={form} bodyshop={bodyshop}/>
<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>
)} )}
{state.sorters && state.sorters.length > 0 && ( {sorters.length > 0 && (
<Card type='inner' title={ t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}> <SortersSection sorters={sorters} form={form}/>
<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>
)} )}
</div> </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 VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import "./report-center-modal.styles.scss"; import "./report-center-modal.styles.scss";
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component"; import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
import {selectBodyshop} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
reportCenterModal: selectReportCenter, reportCenterModal: selectReportCenter,
bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -28,7 +30,7 @@ export default connect(
mapDispatchToProps mapDispatchToProps
)(ReportCenterModalComponent); )(ReportCenterModalComponent);
export function ReportCenterModalComponent({reportCenterModal}) { export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -64,22 +66,28 @@ export function ReportCenterModalComponent({reportCenterModal}) {
const end = values.dates ? values.dates[1] : null; const end = values.dates ? values.dates[1] : null;
const { id } = values; const { id } = values;
await GenerateDocument( const templateConfig = {
{
name: values.key, name: values.key,
variables: { variables: {
...(start ...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") } ? {start: moment(start).startOf("day").format("YYYY-MM-DD")}
: {}), : {}),
...(end ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } : {}), ...(end ? {end: moment(end).endOf("day").format("YYYY-MM-DD")} : {}),
...(start ? { starttz: moment(start).startOf("day") } : {}), ...(start ? {starttz: moment(start).startOf("day")} : {}),
...(end ? { endtz: moment(end).endOf("day") } : {}), ...(end ? {endtz: moment(end).endOf("day")} : {}),
...(id ? { id: id } : {}), ...(id ? {id: id} : {}),
}, },
filters: values.filters, filters: values.filters,
sorters: values.sorters, sorters: values.sorters,
}, };
if (_.isString(values.defaultSorters) && !_.isEmpty(values.defaultSorters)) {
templateConfig.defaultSorters = JSON.parse(values.defaultSorters);
}
await GenerateDocument(
templateConfig,
{ {
to: values.to, to: values.to,
subject: Templates[values.key]?.subject, subject: Templates[values.key]?.subject,
@@ -117,7 +125,8 @@ export function ReportCenterModalComponent({reportCenterModal}) {
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
value={search} value={search}
/> />
<Form.Item <Form.Item name="defaultSorters" hidden/>
<Form.Item
name="key" name="key"
label={t("reportcenter.labels.key")} label={t("reportcenter.labels.key")}
// className="radio-group-columns" // className="radio-group-columns"
@@ -181,7 +190,7 @@ export function ReportCenterModalComponent({reportCenterModal}) {
); );
}} }}
</Form.Item> </Form.Item>
<ReportCenterModalFiltersSortersComponent form={form} /> <ReportCenterModalFiltersSortersComponent form={form} bodyshop={bodyshop} />
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}> <Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => { {() => {
const key = form.getFieldValue("key"); const key = form.getFieldValue("key");
@@ -236,6 +245,9 @@ export function ReportCenterModalComponent({reportCenterModal}) {
{() => { {() => {
const key = form.getFieldValue("key"); const key = form.getFieldValue("key");
const datedisable = Templates[key] && Templates[key].datedisable; const datedisable = Templates[key] && Templates[key].datedisable;
// TODO: MERGE NOTE, Ranges turns to presets in DatePicker.RangePicker
if (datedisable !== true) { if (datedisable !== true) {
return ( return (
<Form.Item <Form.Item
@@ -250,7 +262,7 @@ export function ReportCenterModalComponent({reportCenterModal}) {
> >
<DatePicker.RangePicker <DatePicker.RangePicker
format="MM/DD/YYYY" format="MM/DD/YYYY"
presets={DatePickerRanges} ranges={DatePickerRanges}
/> />
</Form.Item> </Form.Item>
); );

View File

@@ -1,5 +1,14 @@
import { useMutation } from "@apollo/client"; 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 React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries"; import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
@@ -13,6 +22,7 @@ export default function ScoreboardEntryEdit({ entry }) {
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
values.date = moment(values.date).format("YYYY-MM-DD");
const result = await updateScoreboardentry({ const result = await updateScoreboardentry({
variables: { sbId: entry.id, sbInput: values }, variables: { sbId: entry.id, sbInput: values },
}); });
@@ -77,13 +87,14 @@ export default function ScoreboardEntryEdit({ entry }) {
> >
<InputNumber precision={1} /> <InputNumber precision={1} />
</Form.Item> </Form.Item>
<Space wrap>
<Button type="primary" loading={loading} htmlType="submit"> <Button type="primary" loading={loading} htmlType="submit">
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>
<Button onClick={() => setVisible(false)}> <Button onClick={() => setVisible(false)}>
{t("general.actions.cancel")} {t("general.actions.cancel")}
</Button> </Button>
</Space>
</Form> </Form>
</Card> </Card>
); );

View File

@@ -1,3 +1,4 @@
import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Button, Card, Input, Modal, Space, Table, Typography } from "antd"; import { Button, Card, Input, Modal, Space, Table, Typography } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
@@ -5,12 +6,14 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries"; import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component"; 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 ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.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 }) { export default function ScoreboardJobsList({ scoreBoardlist }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
@@ -44,6 +47,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
render: (text, record) => ( render: (text, record) => (
<Link to={"/manage/jobs/" + record.job.id}> <Link to={"/manage/jobs/" + record.job.id}>
{record.job.ro_number || t("general.labels.na")} {record.job.ro_number || t("general.labels.na")}
@@ -55,7 +59,11 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
render: (text, record) => <OwnerNameDisplay ownerObject={record.job} />, render: (text, record) => <OwnerNameDisplay ownerObject={record.job} />,
}, },
{ {
@@ -63,6 +71,15 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, 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) => ( render: (text, record) => (
<span>{`${record.job.v_model_yr || ""} ${ <span>{`${record.job.v_model_yr || ""} ${
record.job.v_make_desc || "" record.job.v_make_desc || ""
@@ -73,17 +90,20 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
title: t("scoreboard.fields.date"), title: t("scoreboard.fields.date"),
dataIndex: "date", dataIndex: "date",
key: "date", key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>, render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
}, },
{
title: t("scoreboard.fields.painthrs"),
dataIndex: "painthrs",
key: "painthrs",
},
{ {
title: t("scoreboard.fields.bodyhrs"), title: t("scoreboard.fields.bodyhrs"),
dataIndex: "bodyhrs", dataIndex: "bodyhrs",
key: "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"), title: t("general.labels.actions"),
@@ -104,8 +124,9 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
visible={state.visible} visible={state.visible}
destroyOnClose destroyOnClose
width="80%" width="80%"
closable={false}
cancelButtonProps={{ style: { display: "none" } }} cancelButtonProps={{ style: { display: "none" } }}
onCancel={() => onOk={() =>
setState((state) => ({ setState((state) => ({
...state, ...state,
visible: false, visible: false,

View File

@@ -22,6 +22,7 @@ export const QUERY_AVAILABLE_CC = gql`
] ]
status: { _eq: "courtesycars.status.in" } status: { _eq: "courtesycars.status.in" }
} }
order_by: { fleetnumber: asc }
) { ) {
color color
dailycost dailycost
@@ -29,6 +30,7 @@ export const QUERY_AVAILABLE_CC = gql`
fleetnumber fleetnumber
fuel fuel
id id
insuranceexpires
make make
mileage mileage
model model
@@ -57,7 +59,7 @@ export const CHECK_CC_FLEET_NUMBER = gql`
`; `;
export const QUERY_ALL_CC = gql` export const QUERY_ALL_CC = gql`
query QUERY_ALL_CC { query QUERY_ALL_CC {
courtesycars { courtesycars(order_by: { fleetnumber: asc }) {
color color
created_at created_at
dailycost dailycost

View File

@@ -747,6 +747,7 @@
"driverinformation": "Driver's Information", "driverinformation": "Driver's Information",
"findcontract": "Find Contract", "findcontract": "Find Contract",
"findermodal": "Contract Finder", "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}}.", "noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.",
"populatefromjob": "Populate from Job", "populatefromjob": "Populate from Job",
"rates": "Contract Rates", "rates": "Contract Rates",
@@ -874,6 +875,7 @@
"labels": { "labels": {
"bodyhrs": "Body Hrs", "bodyhrs": "Body Hrs",
"dollarsinproduction": "Dollars in Production", "dollarsinproduction": "Dollars in Production",
"phone": "Phone",
"prodhrs": "Production Hrs", "prodhrs": "Production Hrs",
"refhrs": "Refinish Hrs" "refhrs": "Refinish Hrs"
}, },
@@ -889,8 +891,10 @@
"productiondollars": "Total Dollars in Production", "productiondollars": "Total Dollars in Production",
"productionhours": "Total Hours in Production", "productionhours": "Total Hours in Production",
"projectedmonthlysales": "Projected Monthly Sales", "projectedmonthlysales": "Projected Monthly Sales",
"scheduledintoday": "Sheduled In Today: {{date}}", "scheduledindate": "Sheduled In Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today: {{date}}" "scheduledintoday": "Sheduled In Today",
"scheduledoutdate": "Sheduled Out Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today"
} }
}, },
"dms": { "dms": {
@@ -2579,6 +2583,11 @@
"advanced_filters_hide": "Hide", "advanced_filters_hide": "Hide",
"advanced_filters_filters": "Filters", "advanced_filters_filters": "Filters",
"advanced_filters_sorters": "Sorters", "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", "dates": "Dates",
"employee": "Employee", "employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}", "filterson": "Filters on {{object}}: {{field}}",

View File

@@ -747,6 +747,7 @@
"driverinformation": "", "driverinformation": "",
"findcontract": "", "findcontract": "",
"findermodal": "", "findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "", "noteconvertedfrom": "",
"populatefromjob": "", "populatefromjob": "",
"rates": "", "rates": "",
@@ -874,6 +875,7 @@
"labels": { "labels": {
"bodyhrs": "", "bodyhrs": "",
"dollarsinproduction": "", "dollarsinproduction": "",
"phone": "",
"prodhrs": "", "prodhrs": "",
"refhrs": "" "refhrs": ""
}, },
@@ -889,7 +891,9 @@
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "", "projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "",
"scheduledouttoday": "" "scheduledouttoday": ""
} }
}, },
@@ -2579,6 +2583,11 @@
"advanced_filters_hide": "", "advanced_filters_hide": "",
"advanced_filters_filters": "", "advanced_filters_filters": "",
"advanced_filters_sorters": "", "advanced_filters_sorters": "",
"advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",

View File

@@ -747,6 +747,7 @@
"driverinformation": "", "driverinformation": "",
"findcontract": "", "findcontract": "",
"findermodal": "", "findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "", "noteconvertedfrom": "",
"populatefromjob": "", "populatefromjob": "",
"rates": "", "rates": "",
@@ -874,6 +875,7 @@
"labels": { "labels": {
"bodyhrs": "", "bodyhrs": "",
"dollarsinproduction": "", "dollarsinproduction": "",
"phone": "",
"prodhrs": "", "prodhrs": "",
"refhrs": "" "refhrs": ""
}, },
@@ -889,7 +891,9 @@
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "", "projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "",
"scheduledouttoday": "" "scheduledouttoday": ""
} }
}, },
@@ -2579,6 +2583,11 @@
"advanced_filters_hide": "", "advanced_filters_hide": "",
"advanced_filters_filters": "", "advanced_filters_filters": "",
"advanced_filters_sorters": "", "advanced_filters_sorters": "",
"advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",

View File

@@ -24,4 +24,13 @@ const range = {
], ],
"Last 90 Days": [moment().add(-90, "days"), moment()], "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; export default range;

View File

@@ -9,7 +9,7 @@ import {store} from "../redux/store";
import client from "../utils/GraphQLClient"; import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios"; import cleanAxios from "./CleanAxios";
import {TemplateList} from "./TemplateConstants"; 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; const server = process.env.REACT_APP_REPORTS_SERVER_URL;
@@ -75,7 +75,10 @@ export default async function RenderTemplate(
headerpath: `/${bodyshop.imexshopid}/header.html`, headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`, footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop, bodyshop: bodyshop,
filters: templateObject?.filters,
sorters: templateObject?.sorters,
offset: bodyshop.timezone, //dayjs().utcOffset(), offset: bodyshop.timezone, //dayjs().utcOffset(),
defaultSorters: templateObject?.defaultSorters,
}, },
}; };
@@ -278,7 +281,9 @@ export const GenerateDocument = async (
sendType, sendType,
jobid jobid
) => { ) => {
const bodyshop = store.getState().user.bodyshop; const bodyshop = store.getState().user.bodyshop;
if (sendType === "e") { if (sendType === "e") {
store.dispatch( store.dispatch(
setEmailOptions({ setEmailOptions({
@@ -402,9 +407,12 @@ const fetchContextData = async (templateObject, jsrAuth) => {
// console.log('Unmodified Query'); // console.log('Unmodified Query');
// console.dir(templateQueryToExecute); // 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 // 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 = {}; let contextData = {};
if (templateQueryToExecute) { if (templateQueryToExecute) {
const {data} = await client.query({ const {data} = await client.query({
@@ -417,36 +425,11 @@ const fetchContextData = async (templateObject, jsrAuth) => {
return {contextData, useShopSpecificTemplate}; return {contextData, useShopSpecificTemplate};
} }
// Parse the query and apply the filters and sorters return await generateTemplate(
const ast = parseQuery(templateQueryToExecute); templateQueryToExecute,
templateObject,
let filterFields = []; useShopSpecificTemplate
);
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};
}; };
//export const displayTemplateInWindow = (html) => { //export const displayTemplateInWindow = (html) => {

View File

@@ -1,4 +1,6 @@
import {Kind, parse, print, visit} from "graphql"; import {Kind, parse, print, visit} from "graphql";
import client from "./GraphQLClient";
import {gql} from "@apollo/client";
const STRING_OPERATORS = [ const STRING_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
@@ -16,8 +18,26 @@ const NUMBER_OPERATORS = [
{value: "_gte", label: "greater than or equal"}, {value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"} {value: "_lte", label: "less than or equal"}
]; ];
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 = { const operators = {
string: STRING_OPERATORS, string: STRING_OPERATORS,
number: NUMBER_OPERATORS number: NUMBER_OPERATORS
@@ -44,6 +64,52 @@ export function parseQuery(query) {
export function printQuery(query) { export function printQuery(query) {
return print(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 * Apply sorters to the AST
* @param ast * @param ast
@@ -261,8 +327,6 @@ export function applyFilters(ast, filters) {
}); });
} }
/** /**
* Get the GraphQL kind for a value * Get the GraphQL kind for a value
* @param value * @param value