Merged in release/2024-02-16 (pull request #1291)

Release/2024 02 16

Approved-by: Patrick Fic
This commit is contained in:
Dave Richer
2024-02-16 20:21:35 +00:00
committed by Patrick Fic
13 changed files with 1352 additions and 412 deletions

View File

@@ -0,0 +1,120 @@
# Filters and Sorters
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports.
## High level Schema Overview
```javascript
const schema = {
"filters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more filters
],
"sorters": [
{
"name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query
"translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI
"label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation
"type": "number" // Type of field, can be number or string currently
},
// ... more sorters
],
"dates": {
// This is not yet implemented and will be added in a future release
}
}
```
## Filters
Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
A note on special notation used in the `name` field.
### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",`
This will produce a where clause at the `joblines` level of the graphQL query,
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query.
```graphql
query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) {
jobs(
where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}}
) {
joblines(
order_by: {line_no: asc}
where: {removed: {_eq: false}}
) {
line_no
mod_lbr_ty
mod_lb_hrs
convertedtolbr
convertedtolbr_data
}
ownr_co_nm
ownr_fn
ownr_ln
plate_no
ro_number
status
v_make_desc
v_model_desc
v_model_yr
v_vin
v_color
}
}
```
## Known Caveats
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The `dates` object is not yet implemented and will be added in a future release.
- The type object must be 'string' or 'number' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- Do not add the ability to filter things that are already filtered as part of the original query, this would be redundant and could cause issues.
- Do not add the ability to filter on things like FK constraints, must like the above example.
## Sorters
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level.
- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the sorters.

View File

@@ -1,16 +1,16 @@
import { useMutation, useLazyQuery } from "@apollo/client";
import { CheckCircleOutlined } from "@ant-design/icons"; import { CheckCircleOutlined } from "@ant-design/icons";
import { useLazyQuery, useMutation } from "@apollo/client";
import { import {
Button, Button,
Card, Card,
Form, Form,
InputNumber, InputNumber,
notification,
Popover, Popover,
Space, Space,
notification,
} from "antd"; } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useState, useEffect } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { import {
@@ -50,6 +50,7 @@ export default function ScoreboardAddButton({
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("job_close_add_to_scoreboard"); logImEXEvent("job_close_add_to_scoreboard");
values.date = moment(values.date).format("YYYY-MM-DD");
setLoading(true); setLoading(true);
let result; let result;
@@ -177,7 +178,7 @@ export default function ScoreboardAddButton({
return acc + job.lbr_adjustments[val]; return acc + job.lbr_adjustments[val];
}, 0); }, 0);
form.setFieldsValue({ form.setFieldsValue({
date: new moment(), date: moment(),
bodyhrs: Math.round(v.bodyhrs * 10) / 10, bodyhrs: Math.round(v.bodyhrs * 10) / 10,
painthrs: Math.round(v.painthrs * 10) / 10, painthrs: Math.round(v.painthrs * 10) / 10,
}); });

View File

@@ -6,7 +6,7 @@ import {
useQuery, useQuery,
} from "@apollo/client"; } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import { Col, notification, Row } from "antd"; import { Col, Row, notification } from "antd";
import Axios from "axios"; import Axios from "axios";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import moment from "moment"; import moment from "moment";
@@ -30,8 +30,8 @@ import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import confirmDialog from "../../utils/asyncConfirm";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import confirmDialog from "../../utils/asyncConfirm";
import CriticalPartsScan from "../../utils/criticalPartsScan"; import CriticalPartsScan from "../../utils/criticalPartsScan";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component"; import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
@@ -73,7 +73,15 @@ export function JobsAvailableContainer({
const [selectedJob, setSelectedJob] = useState(null); const [selectedJob, setSelectedJob] = useState(null);
const [selectedOwner, setSelectedOwner] = useState(null); const [selectedOwner, setSelectedOwner] = useState(null);
const [partsQueueToggle, setPartsQueueToggle] = useState(bodyshop.md_functionality_toggles.parts_queue_toggle); const [partsQueueToggle, setPartsQueueToggle] = useState(
bodyshop.md_functionality_toggles.parts_queue_toggle
);
const [updateSchComp, setSchComp] = useState({
actual_in: moment(),
checked: false,
scheduled_completion: moment(),
automatic: false,
});
const [insertLoading, setInsertLoading] = useState(false); const [insertLoading, setInsertLoading] = useState(false);
@@ -197,11 +205,16 @@ export function JobsAvailableContainer({
notification["error"]({ notification["error"]({
message: t("jobs.errors.creating", { error: err.message }), message: t("jobs.errors.creating", { error: err.message }),
}); });
refetch().catch(e => {console.error(`Something went wrong in jobs available table container - ${err.message || ''}`)}); refetch().catch((e) => {
console.error(
`Something went wrong in jobs available table container - ${
err.message || ""
}`
);
});
setInsertLoading(false); setInsertLoading(false);
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
} }
}; };
//Supplement scenario //Supplement scenario
@@ -225,6 +238,22 @@ export function JobsAvailableContainer({
//IO-539 Check for Parts Rate on PAL for SGI use case. //IO-539 Check for Parts Rate on PAL for SGI use case.
await CheckTaxRates(supp, bodyshop); await CheckTaxRates(supp, bodyshop);
if (updateSchComp.checked === true) {
if (updateSchComp.automatic === true) {
const job_hrs = supp.joblines.data.reduce(
(acc, val) => acc + val.mod_lb_hrs,
0
);
const num_days = job_hrs / bodyshop.target_touchtime;
supp.actual_in = updateSchComp.actual_in;
supp.scheduled_completion = moment(
updateSchComp.actual_in
).businessAdd(num_days, "days");
} else {
supp.scheduled_completion = updateSchComp.scheduled_completion;
}
}
delete supp.owner; delete supp.owner;
delete supp.vehicle; delete supp.vehicle;
delete supp.ins_co_nm; delete supp.ins_co_nm;
@@ -261,9 +290,9 @@ export function JobsAvailableContainer({
}, },
}); });
setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle);
if (CriticalPartsScanning.treatment === "on") { if (CriticalPartsScanning.treatment === "on") {
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id); CriticalPartsScan(updateResult.data.update_jobs.returning[0].id);
} }
if (updateResult.errors) { if (updateResult.errors) {
@@ -367,7 +396,6 @@ export function JobsAvailableContainer({
if (error) return <AlertComponent type="error" message={error.message} />; if (error) return <AlertComponent type="error" message={error.message} />;
return ( return (
<LoadingSpinner <LoadingSpinner
loading={insertLoading} loading={insertLoading}
@@ -384,7 +412,6 @@ export function JobsAvailableContainer({
visible={ownerModalVisible} visible={ownerModalVisible}
onOk={onOwnerFindModalOk} onOk={onOwnerFindModalOk}
onCancel={onOwnerModalCancel} onCancel={onOwnerModalCancel}
/> />
<JobsFindModalContainer <JobsFindModalContainer
loading={estDataRaw.loading} loading={estDataRaw.loading}
@@ -398,6 +425,8 @@ export function JobsAvailableContainer({
modalSearchState={modalSearchState} modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle} partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle} setPartsQueueToggle={setPartsQueueToggle}
updateSchComp={updateSchComp}
setSchComp={setSchComp}
/> />
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>

View File

@@ -131,12 +131,10 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
))} ))}
</DataLabel> </DataLabel>
)} )}
<DataLabel label={t("jobs.fields.production_vars.note")}> <DataLabel label={t("jobs.fields.production_vars.note")}>
<ProductionListColumnProductionNote record={job} /> <ProductionListColumnProductionNote record={job} />
</DataLabel> </DataLabel>
<Space wrap>
<Space>
{job.special_coverage_policy && ( {job.special_coverage_policy && (
<Tag color="tomato"> <Tag color="tomato">
<Space> <Space>

View File

@@ -1,9 +1,11 @@
import { SyncOutlined } from "@ant-design/icons"; import { SyncOutlined } from "@ant-design/icons";
import { Checkbox, Divider, Input, Table, Button } from "antd"; import { Button, Checkbox, Divider, Input, Space, Table } from "antd";
import React from "react"; import moment from "moment";
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 PhoneFormatter from "../../utils/PhoneFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter";
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function JobsFindModalComponent({ export default function JobsFindModalComponent({
@@ -16,11 +18,13 @@ export default function JobsFindModalComponent({
jobsListRefetch, jobsListRefetch,
partsQueueToggle, partsQueueToggle,
setPartsQueueToggle, setPartsQueueToggle,
updateSchComp,
setSchComp,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [modalSearch, setModalSearch] = modalSearchState; const [modalSearch, setModalSearch] = modalSearchState;
const [importOptions, setImportOptions] = importOptionsState; const [importOptions, setImportOptions] = importOptionsState;
const [checkUTT, setCheckUTT] = useState(false);
const columns = [ const columns = [
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
@@ -142,6 +146,35 @@ export default function JobsFindModalComponent({
if (record) { if (record) {
if (record.id) { if (record.id) {
setSelectedJob(record.id); setSelectedJob(record.id);
if (record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: record.scheduled_completion,
});
} else {
if (record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: record.actual_in,
scheduled_completion: moment(),
});
}
if (!record.actual_in && record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(record.scheduled_completion),
});
}
if (!record.actual_in && !record.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(),
});
}
}
return; return;
} }
} }
@@ -177,6 +210,35 @@ export default function JobsFindModalComponent({
rowSelection={{ rowSelection={{
onSelect: (props) => { onSelect: (props) => {
setSelectedJob(props.id); setSelectedJob(props.id);
if (props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: props.scheduled_completion,
});
} else {
if (props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: props.actual_in,
scheduled_completion: moment(),
});
}
if (!props.actual_in && props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(props.scheduled_completion),
});
}
if (!props.actual_in && !props.scheduled_completion) {
setSchComp({
...updateSchComp,
actual_in: moment(),
scheduled_completion: moment(),
});
}
}
}, },
type: "radio", type: "radio",
selectedRowKeys: [selectedJob], selectedRowKeys: [selectedJob],
@@ -190,23 +252,58 @@ export default function JobsFindModalComponent({
}} }}
/> />
<Divider /> <Divider />
<Checkbox <Space>
defaultChecked={importOptions.overrideHeader} <Checkbox
onChange={(e) => defaultChecked={importOptions.overrideHeader}
setImportOptions({ onChange={(e) =>
...importOptions, setImportOptions({
overrideHeaders: e.target.checked, ...importOptions,
}) overrideHeaders: e.target.checked,
} })
> }
{t("jobs.labels.override_header")} >
</Checkbox> {t("jobs.labels.override_header")}
<Checkbox </Checkbox>
<Checkbox
checked={partsQueueToggle} checked={partsQueueToggle}
onChange={(e) => setPartsQueueToggle(e.target.checked)} onChange={(e) => setPartsQueueToggle(e.target.checked)}
> >
{t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")} {t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
</Checkbox> </Checkbox>
<Checkbox
checked={updateSchComp.checked}
onChange={(e) =>
setSchComp({ ...updateSchComp, checked: e.target.checked })
}
>
{t("jobs.labels.update_scheduled_completion")}
</Checkbox>
{updateSchComp.checked === true ? (
<>
{checkUTT === false ? (
<FormDateTimePickerComponent
value={updateSchComp.scheduled_completion}
onChange={(e) => {
setSchComp({ ...updateSchComp, scheduled_completion: e });
}}
/>
) : null}
<Checkbox
checked={checkUTT}
onChange={(e) => {
setCheckUTT(e.target.checked);
setSchComp({
...updateSchComp,
scheduled_completion: null,
automatic: true,
});
}}
>
{t("jobs.labels.calc_scheuled_completion")}
</Checkbox>
</>
) : null}
</Space>
</div> </div>
); );
} }

View File

@@ -26,6 +26,8 @@ export default connect(
modalSearchState, modalSearchState,
partsQueueToggle, partsQueueToggle,
setPartsQueueToggle, setPartsQueueToggle,
updateSchComp,
setSchComp,
...modalProps ...modalProps
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -95,6 +97,8 @@ export default connect(
modalSearchState={modalSearchState} modalSearchState={modalSearchState}
partsQueueToggle={partsQueueToggle} partsQueueToggle={partsQueueToggle}
setPartsQueueToggle={setPartsQueueToggle} setPartsQueueToggle={setPartsQueueToggle}
updateSchComp={updateSchComp}
setSchComp={setSchComp}
/> />
) : null} ) : null}
</Modal> </Modal>

View File

@@ -0,0 +1,273 @@
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd";
import React, {useEffect, 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 LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
export default function ReportCenterModalFiltersSortersComponent({form}) {
return (
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
return <RenderFilters form={form} templateId={key}/>;
}}
</Form.Item>
);
}
function RenderFilters({templateId, form}) {
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);
};
if (templateId) {
fetch();
}
}, [templateId]);
// 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
checked={visible}
onChange={(e) => setVisible(e.target.checked)}
children={t('reportcenter.labels.advanced_filters')}
/>
{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>
)}
{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>
)}
</div>
)}
</div>
);
}

View File

@@ -1,30 +1,22 @@
import { useLazyQuery } from "@apollo/client"; import {useLazyQuery} from "@apollo/client";
import { import {Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography,} from "antd";
Button,
Card,
Col,
DatePicker,
Form,
Input,
Radio,
Row,
Typography,
} from "antd";
import _ from "lodash"; import _ from "lodash";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, {useState} from "react";
import { useTranslation } from "react-i18next"; import {useTranslation} from "react-i18next";
import { connect } from "react-redux"; import {connect} from "react-redux";
import { createStructuredSelector } from "reselect"; import {createStructuredSelector} from "reselect";
import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries"; import {QUERY_ACTIVE_EMPLOYEES} from "../../graphql/employees.queries";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries"; import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries";
import { selectReportCenter } from "../../redux/modals/modals.selectors"; import {selectReportCenter} from "../../redux/modals/modals.selectors";
import DatePIckerRanges from "../../utils/DatePickerRanges"; import DatePickerRanges from "../../utils/DatePickerRanges";
import { GenerateDocument } from "../../utils/RenderTemplate"; import {GenerateDocument} from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import {TemplateList} from "../../utils/TemplateConstants";
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component"; import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
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";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
reportCenterModal: selectReportCenter, reportCenterModal: selectReportCenter,
}); });
@@ -32,39 +24,39 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(ReportCenterModalComponent); )(ReportCenterModalComponent);
export function ReportCenterModalComponent({ reportCenterModal }) { export function ReportCenterModalComponent({reportCenterModal}) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t } = useTranslation(); const {t} = useTranslation();
const Templates = TemplateList("report_center"); const Templates = TemplateList("report_center");
const ReportsList = Object.keys(Templates).map((key) => { const ReportsList = Object.keys(Templates).map((key) => {
return Templates[key]; return Templates[key];
}); });
const { visible } = reportCenterModal; const {open} = reportCenterModal;
const [callVendorQuery, { data: vendorData, called: vendorCalled }] = const [callVendorQuery, {data: vendorData, called: vendorCalled}] =
useLazyQuery(QUERY_ALL_VENDORS, { useLazyQuery(QUERY_ALL_VENDORS, {
skip: !( skip: !(
visible && open &&
Templates[form.getFieldValue("key")] && Templates[form.getFieldValue("key")] &&
Templates[form.getFieldValue("key")].idtype Templates[form.getFieldValue("key")].idtype
), ),
}); });
const [callEmployeeQuery, { data: employeeData, called: employeeCalled }] = const [callEmployeeQuery, {data: employeeData, called: employeeCalled}] =
useLazyQuery(QUERY_ACTIVE_EMPLOYEES, { useLazyQuery(QUERY_ACTIVE_EMPLOYEES, {
skip: !( skip: !(
visible && open &&
Templates[form.getFieldValue("key")] && Templates[form.getFieldValue("key")] &&
Templates[form.getFieldValue("key")].idtype Templates[form.getFieldValue("key")].idtype
), ),
}); });
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
@@ -73,243 +65,245 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
const { id } = values; const { id } = values;
await GenerateDocument( await GenerateDocument(
{ {
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 ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } : {}),
? { end: moment(end).endOf("day").format("YYYY-MM-DD") } ...(start ? { starttz: moment(start).startOf("day") } : {}),
: {}), ...(end ? { endtz: moment(end).endOf("day") } : {}),
...(start ? { starttz: moment(start).startOf("day") } : {}),
...(end ? { endtz: moment(end).endOf("day") } : {}),
...(id ? { id: id } : {}), ...(id ? { id: id } : {}),
},
filters: values.filters,
sorters: values.sorters,
}, },
}, {
{ to: values.to,
to: values.to, subject: Templates[values.key]?.subject,
subject: Templates[values.key]?.subject, },
}, values.sendbyexcel === "excel"
values.sendbyexcel === "excel" ? "x"
? "x" : values.sendby === "email"
: values.sendby === "email" ? "e"
? "e" : "p",
: "p", id
id
); );
setLoading(false); setLoading(false);
}; };
const FilteredReportsList = const FilteredReportsList =
search !== "" search !== ""
? ReportsList.filter((r) => ? ReportsList.filter((r) =>
r.title.toLowerCase().includes(search.toLowerCase()) r.title.toLowerCase().includes(search.toLowerCase())
) )
: ReportsList; : ReportsList;
//Group it, create cards, and then filter out. //Group it, create cards, and then filter out.
const grouped = _.groupBy(FilteredReportsList, "group"); const grouped = _.groupBy(FilteredReportsList, "group");
return ( return (
<div> <div>
<Form <Form
onFinish={handleFinish} onFinish={handleFinish}
autoComplete={"off"} autoComplete={"off"}
layout="vertical" layout="vertical"
form={form} form={form}
>
<Input.Search
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
<Form.Item
name="key"
label={t("reportcenter.labels.key")}
// className="radio-group-columns"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
> >
<Radio.Group> <Input.Search
{/* {Object.keys(Templates).map((key) => ( onChange={(e) => setSearch(e.target.value)}
value={search}
/>
<Form.Item
name="key"
label={t("reportcenter.labels.key")}
// className="radio-group-columns"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Radio.Group>
{/* {Object.keys(Templates).map((key) => (
<Radio key={key} value={key}> <Radio key={key} value={key}>
{Templates[key].title} {Templates[key].title}
</Radio> </Radio>
))} */} ))} */}
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{Object.keys(grouped).map((key) => ( {Object.keys(grouped).map((key) => (
<Col md={8} sm={12} key={key}> <Col md={8} sm={12} key={key}>
<Card.Grid <Card.Grid
style={{ style={{
width: "100%", width: "100%",
height: "100%", height: "100%",
maxHeight: "33vh", maxHeight: "33vh",
overflowY: "scroll", overflowY: "scroll",
}} }}
> >
<Typography.Title level={4}> <Typography.Title level={4}>
{t(`reportcenter.labels.groups.${key}`)} {t(`reportcenter.labels.groups.${key}`)}
</Typography.Title> </Typography.Title>
<ul style={{ columns: "2 auto" }}> <ul style={{ listStyleType: 'none', columns: "2 auto"}}>
{grouped[key].map((item) => ( {grouped[key].map((item) => (
<li key={item.key}> <li key={item.key}>
<Radio key={item.key} value={item.key}> <Radio key={item.key} value={item.key}>
{item.title} {item.title}
</Radio> </Radio>
</li> </li>
))} ))}
</ul> </ul>
</Card.Grid> </Card.Grid>
</Col> </Col>
))} ))}
</Row> </Row>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<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");
if (!key) return null; if (!key) return null;
//Kind of Id //Kind of Id
const rangeFilter = Templates[key] && Templates[key].rangeFilter; const rangeFilter = Templates[key] && Templates[key].rangeFilter;
if (!rangeFilter) return null; if (!rangeFilter) return null;
return (
<div>
{t("reportcenter.labels.filterson", {
object: rangeFilter.object,
field: rangeFilter.field,
})}
</div>
);
}}
</Form.Item>
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
const currentId = form.getFieldValue("id");
if (!key) return null;
//Kind of Id
const idtype = Templates[key] && Templates[key].idtype;
if (!idtype && currentId) {
form.setFieldsValue({ id: null });
return null;
}
if (!vendorCalled && idtype === "vendor") callVendorQuery();
if (!employeeCalled && idtype === "employee") callEmployeeQuery();
if (idtype === "vendor")
return ( return (
<Form.Item <div>
name="id" {t("reportcenter.labels.filterson", {
label={t("reportcenter.labels.vendor")} object: rangeFilter.object,
rules={[ field: rangeFilter.field,
{ })}
required: true, </div>
//message: t("general.validation.required"),
},
]}
>
<VendorSearchSelect
options={vendorData ? vendorData.vendors : []}
/>
</Form.Item>
); );
if (idtype === "employee") }}
return ( </Form.Item>
<Form.Item <ReportCenterModalFiltersSortersComponent form={form} />
name="id" <Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
label={t("reportcenter.labels.employee")} {() => {
rules={[ const key = form.getFieldValue("key");
{ const currentId = form.getFieldValue("id");
required: true, if (!key) return null;
//message: t("general.validation.required"), //Kind of Id
}, const idtype = Templates[key] && Templates[key].idtype;
]} if (!idtype && currentId) {
> form.setFieldsValue({id: null});
<EmployeeSearchSelect return null;
options={employeeData ? employeeData.employees : []} }
/> if (!vendorCalled && idtype === "vendor") callVendorQuery();
</Form.Item> if (!employeeCalled && idtype === "employee") callEmployeeQuery();
); if (idtype === "vendor")
else return null; return (
}} <Form.Item
</Form.Item> name="id"
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}> label={t("reportcenter.labels.vendor")}
{() => { rules={[
const key = form.getFieldValue("key"); {
const datedisable = Templates[key] && Templates[key].datedisable; required: true,
if (datedisable !== true) { //message: t("general.validation.required"),
return ( },
<Form.Item ]}
name="dates" >
label={t("reportcenter.labels.dates")} <VendorSearchSelect
rules={[ options={vendorData ? vendorData.vendors : []}
{ />
required: true, </Form.Item>
//message: t("general.validation.required"), );
}, if (idtype === "employee")
]} return (
> <Form.Item
<DatePicker.RangePicker name="id"
format="MM/DD/YYYY" label={t("reportcenter.labels.employee")}
ranges={DatePIckerRanges} rules={[
/> {
</Form.Item> required: true,
); //message: t("general.validation.required"),
} else return null; },
}} ]}
</Form.Item> >
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}> <EmployeeSearchSelect
{() => { options={employeeData ? employeeData.employees : []}
const key = form.getFieldValue("key"); />
//Kind of Id </Form.Item>
const reporttype = Templates[key] && Templates[key].reporttype; );
else return null;
}}
</Form.Item>
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
const datedisable = Templates[key] && Templates[key].datedisable;
if (datedisable !== true) {
return (
<Form.Item
name="dates"
label={t("reportcenter.labels.dates")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<DatePicker.RangePicker
format="MM/DD/YYYY"
presets={DatePickerRanges}
/>
</Form.Item>
);
} else return null;
}}
</Form.Item>
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => {
const key = form.getFieldValue("key");
//Kind of Id
const reporttype = Templates[key] && Templates[key].reporttype;
if (reporttype === "excel") if (reporttype === "excel")
return ( return (
<Form.Item <Form.Item
label={t("general.labels.sendby")} label={t("general.labels.sendby")}
name="sendbyexcel" name="sendbyexcel"
initialValue="excel" initialValue="excel"
> >
<Radio.Group> <Radio.Group>
<Radio value="excel">{t("general.labels.excel")}</Radio> <Radio value="excel">{t("general.labels.excel")}</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
); );
if (reporttype !== "excel") if (reporttype !== "excel")
return ( return (
<Form.Item <Form.Item
label={t("general.labels.sendby")} label={t("general.labels.sendby")}
name="sendby" name="sendby"
initialValue="print" initialValue="print"
> >
<Radio.Group> <Radio.Group>
<Radio value="email">{t("general.labels.email")}</Radio> <Radio value="email">{t("general.labels.email")}</Radio>
<Radio value="print">{t("general.labels.print")}</Radio> <Radio value="print">{t("general.labels.print")}</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
); );
}} }}
</Form.Item> </Form.Item>
<div <div
style={{ style={{
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
marginTop: "1rem", marginTop: "1rem",
}} }}
> >
<Button onClick={() => form.submit()} style={{}} loading={loading}> <Button onClick={() => form.submit()} style={{}} loading={loading}>
{t("reportcenter.actions.generate")} {t("reportcenter.actions.generate")}
</Button> </Button>
</div> </div>
</Form> </Form>
</div> </div>
); );
} }

View File

@@ -1732,6 +1732,7 @@
"ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ", "ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ",
"calc_repair_days": "Calculated Repair Days", "calc_repair_days": "Calculated Repair Days",
"calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).", "calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).",
"calc_scheuled_completion": "Calculate Scheduled Completion",
"cards": { "cards": {
"customer": "Customer Information", "customer": "Customer Information",
"damage": "Area of Damage", "damage": "Area of Damage",
@@ -1891,6 +1892,7 @@
"total_sales": "Total Sales", "total_sales": "Total Sales",
"totals": "Totals", "totals": "Totals",
"unvoidnote": "This Job was unvoided.", "unvoidnote": "This Job was unvoided.",
"update_scheduled_completion": "Update Scheduled Completion?",
"vehicle_info": "Vehicle", "vehicle_info": "Vehicle",
"vehicleassociation": "Vehicle Association", "vehicleassociation": "Vehicle Association",
"viewallocations": "View Allocations", "viewallocations": "View Allocations",
@@ -2572,6 +2574,11 @@
"generate": "Generate" "generate": "Generate"
}, },
"labels": { "labels": {
"advanced_filters": "Advanced Filters and Sorters",
"advanced_filters_show": "Show",
"advanced_filters_hide": "Hide",
"advanced_filters_filters": "Filters",
"advanced_filters_sorters": "Sorters",
"dates": "Dates", "dates": "Dates",
"employee": "Employee", "employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}", "filterson": "Filters on {{object}}: {{field}}",

View File

@@ -1732,6 +1732,7 @@
"ca_gst_all_if_null": "", "ca_gst_all_if_null": "",
"calc_repair_days": "", "calc_repair_days": "",
"calc_repair_days_tt": "", "calc_repair_days_tt": "",
"calc_scheuled_completion": "",
"cards": { "cards": {
"customer": "Información al cliente", "customer": "Información al cliente",
"damage": "Área de Daño", "damage": "Área de Daño",
@@ -1891,6 +1892,7 @@
"total_sales": "", "total_sales": "",
"totals": "", "totals": "",
"unvoidnote": "", "unvoidnote": "",
"update_scheduled_completion": "",
"vehicle_info": "Vehículo", "vehicle_info": "Vehículo",
"vehicleassociation": "", "vehicleassociation": "",
"viewallocations": "", "viewallocations": "",
@@ -2572,6 +2574,11 @@
"generate": "" "generate": ""
}, },
"labels": { "labels": {
"advanced_filters": "",
"advanced_filters_show": "",
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",

View File

@@ -1732,6 +1732,7 @@
"ca_gst_all_if_null": "", "ca_gst_all_if_null": "",
"calc_repair_days": "", "calc_repair_days": "",
"calc_repair_days_tt": "", "calc_repair_days_tt": "",
"calc_scheuled_completion": "",
"cards": { "cards": {
"customer": "Informations client", "customer": "Informations client",
"damage": "Zone de dommages", "damage": "Zone de dommages",
@@ -1891,6 +1892,7 @@
"total_sales": "", "total_sales": "",
"totals": "", "totals": "",
"unvoidnote": "", "unvoidnote": "",
"update_scheduled_completion": "",
"vehicle_info": "Véhicule", "vehicle_info": "Véhicule",
"vehicleassociation": "", "vehicleassociation": "",
"viewallocations": "", "viewallocations": "",
@@ -2572,6 +2574,11 @@
"generate": "" "generate": ""
}, },
"labels": { "labels": {
"advanced_filters": "",
"advanced_filters_show": "",
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",

View File

@@ -1,14 +1,16 @@
import { gql } from "@apollo/client"; import {gql} from "@apollo/client";
import jsreport from "@jsreport/browser-client"; import jsreport from "@jsreport/browser-client";
import { notification } from "antd"; import {notification} from "antd";
import axios from "axios"; import axios from "axios";
import _ from "lodash"; import _ from "lodash";
import { auth } from "../firebase/firebase.utils"; import {auth} from "../firebase/firebase.utils";
import { setEmailOptions } from "../redux/email/email.actions"; import {setEmailOptions} from "../redux/email/email.actions";
import { store } from "../redux/store"; 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";
const server = process.env.REACT_APP_REPORTS_SERVER_URL; const server = process.env.REACT_APP_REPORTS_SERVER_URL;
jsreport.serverUrl = server; jsreport.serverUrl = server;
@@ -16,11 +18,11 @@ jsreport.serverUrl = server;
const Templates = TemplateList(); const Templates = TemplateList();
export default async function RenderTemplate( export default async function RenderTemplate(
templateObject, templateObject,
bodyshop, bodyshop,
renderAsHtml = false, renderAsHtml = false,
renderAsExcel = false, renderAsExcel = false,
renderAsText = false renderAsText = false
) { ) {
if (window.jsr3) { if (window.jsr3) {
jsreport.serverUrl = "https://reports3.test.imex.online/"; jsreport.serverUrl = "https://reports3.test.imex.online/";
@@ -30,41 +32,41 @@ export default async function RenderTemplate(
jsreport.headers["Authorization"] = jsrAuth; jsreport.headers["Authorization"] = jsrAuth;
//Query assets that match the template name. Must be in format <<templateName>>.query //Query assets that match the template name. Must be in format <<templateName>>.query
let { contextData, useShopSpecificTemplate } = await fetchContextData( let {contextData, useShopSpecificTemplate} = await fetchContextData(
templateObject, templateObject,
jsrAuth jsrAuth
); );
const { ignoreCustomMargins } = Templates[templateObject.name]; const {ignoreCustomMargins} = Templates[templateObject.name];
let reportRequest = { let reportRequest = {
template: { template: {
name: useShopSpecificTemplate name: useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${templateObject.name}` ? `/${bodyshop.imexshopid}/${templateObject.name}`
: `/${templateObject.name}`, : `/${templateObject.name}`,
...(renderAsHtml ...(renderAsHtml
? {} ? {}
: { : {
recipe: "chrome-pdf", recipe: "chrome-pdf",
...(!ignoreCustomMargins && { ...(!ignoreCustomMargins && {
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px", : "50px",
}, },
}), }),
}), }),
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}), ...(renderAsExcel ? {recipe: "html-to-xlsx"} : {}),
...(renderAsText ? { recipe: "text" } : {}), ...(renderAsText ? {recipe: "text"} : {}),
}, },
data: { data: {
...contextData, ...contextData,
@@ -73,7 +75,7 @@ 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,
offset: bodyshop.timezone, //moment().utcOffset(), offset: bodyshop.timezone, //dayjs().utcOffset(),
}, },
}; };
@@ -82,8 +84,8 @@ export default async function RenderTemplate(
if (!renderAsHtml) { if (!renderAsHtml) {
render.download( render.download(
(Templates[templateObject.name] && (Templates[templateObject.name] &&
Templates[templateObject.name].title) || Templates[templateObject.name].title) ||
"" ""
); );
} else { } else {
@@ -97,17 +99,17 @@ export default async function RenderTemplate(
...(!ignoreCustomMargins && { ...(!ignoreCustomMargins && {
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px", : "50px",
}, },
}), }),
}, },
@@ -121,21 +123,21 @@ export default async function RenderTemplate(
resolve({ resolve({
pdf, pdf,
filename: filename:
Templates[templateObject.name] && Templates[templateObject.name] &&
Templates[templateObject.name].title, Templates[templateObject.name].title,
html, html,
}); });
}); });
} }
} catch (error) { } catch (error) {
notification["error"]({ message: JSON.stringify(error) }); notification["error"]({message: JSON.stringify(error)});
} }
} }
export async function RenderTemplates( export async function RenderTemplates(
templateObjects, templateObjects,
bodyshop, bodyshop,
renderAsHtml = false renderAsHtml = false
) { ) {
//Query assets that match the template name. Must be in format <<templateName>>.query //Query assets that match the template name. Must be in format <<templateName>>.query
let unsortedTemplatesAndData = []; let unsortedTemplatesAndData = [];
@@ -145,17 +147,17 @@ export async function RenderTemplates(
templateObjects.forEach((template) => { templateObjects.forEach((template) => {
proms.push( proms.push(
(async () => { (async () => {
let { contextData, useShopSpecificTemplate } = await fetchContextData( let {contextData, useShopSpecificTemplate} = await fetchContextData(
template, template,
jsrAuth jsrAuth
); );
unsortedTemplatesAndData.push({ unsortedTemplatesAndData.push({
templateObject: template, templateObject: template,
contextData, contextData,
useShopSpecificTemplate, useShopSpecificTemplate,
}); });
})() })()
); );
}); });
await Promise.all(proms); await Promise.all(proms);
@@ -172,8 +174,8 @@ export async function RenderTemplates(
unsortedTemplatesAndData.sort(function (a, b) { unsortedTemplatesAndData.sort(function (a, b) {
return ( return (
templateObjects.findIndex((x) => x.name === a.templateObject.name) - templateObjects.findIndex((x) => x.name === a.templateObject.name) -
templateObjects.findIndex((x) => x.name === b.templateObject.name) templateObjects.findIndex((x) => x.name === b.templateObject.name)
); );
}); });
const templateAndData = unsortedTemplatesAndData; const templateAndData = unsortedTemplatesAndData;
@@ -183,25 +185,25 @@ export async function RenderTemplates(
let reportRequest = { let reportRequest = {
template: { template: {
name: rootTemplate.useShopSpecificTemplate name: rootTemplate.useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}` ? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
: `/${rootTemplate.templateObject.name}`, : `/${rootTemplate.templateObject.name}`,
...(renderAsHtml ...(renderAsHtml
? {} ? {}
: { : {
recipe: "chrome-pdf", recipe: "chrome-pdf",
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px", : "50px",
}, },
}), }),
pdfOperations: [ pdfOperations: [
@@ -218,22 +220,22 @@ export async function RenderTemplates(
template: { template: {
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px", : "50px",
}, },
name: template.useShopSpecificTemplate name: template.useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${template.templateObject.name}` ? `/${bodyshop.imexshopid}/${template.templateObject.name}`
: `/${template.templateObject.name}`, : `/${template.templateObject.name}`,
...(renderAsHtml ? {} : { recipe: "chrome-pdf" }), ...(renderAsHtml ? {} : {recipe: "chrome-pdf"}),
}, },
type: "append", type: "append",
@@ -245,8 +247,8 @@ export async function RenderTemplates(
}, },
data: { data: {
...extend( ...extend(
rootTemplate.contextData, rootTemplate.contextData,
...templateAndData.map((temp) => temp.contextData) ...templateAndData.map((temp) => temp.contextData)
), ),
// ...rootTemplate.templateObject.variables, // ...rootTemplate.templateObject.variables,
@@ -266,32 +268,31 @@ export async function RenderTemplates(
return render.toString(); return render.toString();
} }
} catch (error) { } catch (error) {
notification["error"]({ message: JSON.stringify(error) }); notification["error"]({message: JSON.stringify(error)});
} }
} }
export const GenerateDocument = async ( export const GenerateDocument = async (
template, template,
messageOptions, messageOptions,
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({
jobid, jobid,
messageOptions: { messageOptions: {
...messageOptions, ...messageOptions,
to: Array.isArray(messageOptions.to) to: Array.isArray(messageOptions.to)
? messageOptions.to ? messageOptions.to
: [messageOptions.to], : [messageOptions.to],
}, },
template, template,
}) })
); );
} else if (sendType === "x") { } else if (sendType === "x") {
console.log("excel");
await RenderTemplate(template, bodyshop, false, true); await RenderTemplate(template, bodyshop, false, true);
} else if (sendType === "text") { } else if (sendType === "text") {
await RenderTemplate(template, bodyshop, false, false, true); await RenderTemplate(template, bodyshop, false, false, true);
@@ -305,22 +306,74 @@ export const GenerateDocuments = async (templates) => {
await RenderTemplates(templates, bodyshop); await RenderTemplates(templates, bodyshop);
}; };
export const fetchFilterData = async ({name}) => {
try {
const bodyshop = store.getState().user.bodyshop;
const jsrAuth = (await axios.post("/utils/jsr")).data;
jsreport.headers["FirebaseAuthorization"] =
"Bearer " + (await auth.currentUser.getIdToken());
const folders = await cleanAxios.get(`${server}/odata/folders`, {
headers: {Authorization: jsrAuth},
});
const shopSpecificFolder = folders.data.value.find(
(f) => f.name === bodyshop.imexshopid
);
const jsReportFilters = await cleanAxios.get(
`${server}/odata/assets?$filter=name eq '${name}.filters'`,
{headers: {Authorization: jsrAuth}}
);
console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
let parsedFilterData;
let useShopSpecificTemplate = false;
// let shopSpecificTemplate;
if (shopSpecificFolder) {
let shopSpecificTemplate = jsReportFilters.data.value.find(
(f) => f?.folder?.shortid === shopSpecificFolder.shortid
);
if (shopSpecificTemplate) {
useShopSpecificTemplate = true;
parsedFilterData = atob(shopSpecificTemplate.content);
}
}
if (!parsedFilterData) {
const generalTemplate = jsReportFilters.data.value.find((f) => !f.folder);
useShopSpecificTemplate = false;
if (generalTemplate) parsedFilterData = atob(generalTemplate.content);
}
const data = JSON.parse(parsedFilterData);
return {
data,
useShopSpecificTemplate,
success: true,
}
} catch {
return {
success: false,
}
}
};
const fetchContextData = async (templateObject, jsrAuth) => { const fetchContextData = async (templateObject, jsrAuth) => {
const bodyshop = store.getState().user.bodyshop; const bodyshop = store.getState().user.bodyshop;
jsreport.headers["FirebaseAuthorization"] = jsreport.headers["FirebaseAuthorization"] =
"Bearer " + (await auth.currentUser.getIdToken()); "Bearer " + (await auth.currentUser.getIdToken());
const folders = await cleanAxios.get(`${server}/odata/folders`, { const folders = await cleanAxios.get(`${server}/odata/folders`, {
headers: { Authorization: jsrAuth }, headers: {Authorization: jsrAuth},
}); });
const shopSpecificFolder = folders.data.value.find( const shopSpecificFolder = folders.data.value.find(
(f) => f.name === bodyshop.imexshopid (f) => f.name === bodyshop.imexshopid
); );
const jsReportQueries = await cleanAxios.get( const jsReportQueries = await cleanAxios.get(
`${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`, `${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
{ headers: { Authorization: jsrAuth } } {headers: {Authorization: jsrAuth}}
); );
let templateQueryToExecute; let templateQueryToExecute;
@@ -329,7 +382,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
if (shopSpecificFolder) { if (shopSpecificFolder) {
let shopSpecificTemplate = jsReportQueries.data.value.find( let shopSpecificTemplate = jsReportQueries.data.value.find(
(f) => f?.folder?.shortid === shopSpecificFolder.shortid (f) => f?.folder?.shortid === shopSpecificFolder.shortid
); );
if (shopSpecificTemplate) { if (shopSpecificTemplate) {
useShopSpecificTemplate = true; useShopSpecificTemplate = true;
@@ -343,16 +396,57 @@ const fetchContextData = async (templateObject, jsrAuth) => {
templateQueryToExecute = atob(generalTemplate.content); templateQueryToExecute = atob(generalTemplate.content);
} }
// Commented out for future revision debugging
// console.log('Template Object');
// console.dir(templateObject);
// console.log('Unmodified Query');
// console.dir(templateQueryToExecute);
// 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)) {
let contextData = {};
if (templateQueryToExecute) {
const {data} = await client.query({
query: gql(templateQueryToExecute),
variables: {...templateObject.variables},
});
contextData = data;
}
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 = {}; let contextData = {};
if (templateQueryToExecute) { if (templateQueryToExecute) {
const { data } = await client.query({ const {data} = await client.query({
query: gql(templateQueryToExecute), query: gql(finalQuery),
variables: { ...templateObject.variables }, variables: {...templateObject.variables},
}); });
contextData = data; contextData = data;
} }
return { contextData, useShopSpecificTemplate }; return {contextData, useShopSpecificTemplate};
}; };
//export const displayTemplateInWindow = (html) => { //export const displayTemplateInWindow = (html) => {
@@ -389,7 +483,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
function extend(o1, o2, o3) { function extend(o1, o2, o3) {
var result = {}, var result = {},
obj; obj;
for (var i = 0; i < arguments.length; i++) { for (var i = 0; i < arguments.length; i++) {
obj = arguments[i]; obj = arguments[i];
@@ -405,4 +499,4 @@ function extend(o1, o2, o3) {
} }
} }
return result; return result;
} }

View File

@@ -0,0 +1,309 @@
import {Kind, parse, print, visit} from "graphql";
const STRING_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_like", label: "contains"},
{value: "_nlike", label: "does not contain"},
{value: "_ilike", label: "contains case-insensitive"},
{value: "_nilike", label: "does not contain case-insensitive"}
];
const NUMBER_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"}
];
export function getOperatorsByType(type = 'string') {
const operators = {
string: STRING_OPERATORS,
number: NUMBER_OPERATORS
};
return operators[type];
}
/* eslint-disable no-loop-func */
/**
* Parse a GraphQL query into an AST
* @param query
* @returns {DocumentNode}
*/
export function parseQuery(query) {
return parse(query);
}
/**
* Print an AST back into a GraphQL query
* @param query
* @returns {string}
*/
export function printQuery(query) {
return print(query);
}
/**
* Apply sorters to the AST
* @param ast
* @param sorters
*/
export function applySorters(ast, sorters) {
sorters.forEach((sorter) => {
const fieldPath = sorter.field.split('.');
visit(ast, {
OperationDefinition: {
enter(node) {
// Loop through each sorter to apply it
// noinspection DuplicatedCode
let currentSelection = node; // Start with the root operation
// Navigate down the field path to the correct location
for (let i = 0; i < fieldPath.length - 1; i++) {
let found = false;
visit(currentSelection, {
Field: {
enter(node) {
if (node.name.value === fieldPath[i]) {
currentSelection = node; // Move down to the next level
found = true;
}
}
}
});
if (!found) break; // Stop if we can't find the next field in the path
}
// Apply the sorter at the correct level
if (currentSelection) {
const targetFieldName = fieldPath[fieldPath.length - 1];
let orderByArg = currentSelection.arguments.find(arg => arg.name.value === 'order_by');
if (!orderByArg) {
orderByArg = {
kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'order_by' },
value: { kind: Kind.OBJECT, fields: [] },
};
currentSelection.arguments.push(orderByArg);
}
const sorterField = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetFieldName },
value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions
};
// Add the new sorter condition
orderByArg.value.fields.push(sorterField);
}
}
}
});
});
}
/**
* Apply filters to the AST
* @param ast
* @param filters
*/
export function applyFilters(ast, filters) {
return visit(ast, {
OperationDefinition: {
enter(node) {
filters.forEach(filter => {
const fieldPath = filter.field.split('.');
let topLevel = false;
// Determine if the filter should be applied at the top level
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevel = true;
}
if (topLevel) {
// Construct the filter for a top-level application
const targetFieldName = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
const nestedFilter = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetFieldName },
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: filter.operator },
value: filterValue,
}],
},
};
// Find or create the where argument for the top-level field
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'where' },
value: { kind: Kind.OBJECT, fields: [] },
};
const topLevelSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: path },
value: { kind: Kind.OBJECT, fields: [] }
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(nestedFilter);
} else { // Directly under the top level
whereArg.value.fields.push(nestedFilter);
}
} else {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
let whereArgFound = false;
// Iterate over the fieldPath, except for the last entry, to navigate the structure
for (let i = 0; i < fieldPath.length - 1; i++) {
const fieldName = fieldPath[i];
let fieldFound = false;
// Check if the current selection has a selectionSet and selections
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
// Look for the field in the current selection's selections
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
if (selection) {
// Move down the AST to the found selection
currentSelection = selection;
fieldFound = true;
}
}
// If the field was not found in the current path, it's an issue
if (!fieldFound) {
console.error(`Field ${fieldName} not found in the current selection.`);
return; // Exit the loop and function due to error
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied
// Check if the 'where' argument already exists in the current selection
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
if (whereArg) {
whereArgFound = true;
} else {
// If not found, create a new 'where' argument for the current selection
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'where' },
value: { kind: Kind.OBJECT, fields: [] } // Empty fields array to be populated with the filter
});
}
// Assuming the last entry in fieldPath is the field to apply the filter on
const targetField = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
// Construct the filter field object
const filterField = {
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetField },
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: filter.operator },
value: filterValue,
}],
},
};
// Add the filter field to the 'where' clause of the current selection
if (whereArgFound) {
whereArg.value.fields.push(filterField);
} else {
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
}
}
});
}
}
});
}
/**
* Get the GraphQL kind for a value
* @param value
* @returns {Kind|Kind.INT}
*/
function getGraphQLKind(value) {
if (typeof value === 'number') {
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
} else if (typeof value === 'boolean') {
return Kind.BOOLEAN;
} else if (typeof value === 'string') {
return Kind.STRING;
}
// Extend with more types as needed
}
/**
* Wrap filters in an 'and' object
* @param ast
* @param filterFields
*/
export function wrapFiltersInAnd(ast, filterFields) {
visit(ast, {
OperationDefinition: {
enter(node) {
node.selectionSet.selections.forEach((selection) => {
let whereArg = selection.arguments.find(arg => arg.name.value === 'where');
if (filterFields.length > 1) {
const andFilter = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: '_and'},
value: {kind: Kind.LIST, values: filterFields}
};
whereArg.value.fields.push(andFilter);
} else if (filterFields.length === 1) {
whereArg.value.fields.push(filterFields[0].fields[0]);
}
});
}
}
});
}
/* eslint-enable no-loop-func */