diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md
new file mode 100644
index 000000000..bcaa08ade
--- /dev/null
+++ b/_reference/reportFiltersAndSorters.md
@@ -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.
diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx
new file mode 100644
index 000000000..98293a607
--- /dev/null
+++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx
@@ -0,0 +1,261 @@
+import {Button, Card, 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";
+
+export default function ReportCenterModalFiltersSortersComponent({form}) {
+ return (
+
+ {() => {
+ const key = form.getFieldValue("key");
+ return ;
+ }}
+
+ );
+}
+
+function RenderFilters({templateId, form}) {
+ const [state, setState] = useState(null);
+ const [visible, setVisible] = useState(false);
+ const {t} = useTranslation();
+
+ useEffect(() => {
+ const fetch = async () => {
+ const data = await fetchFilterData({name: templateId});
+ if (data?.success) {
+ setState(data.data);
+ } else {
+ setState(null);
+ }
+ };
+
+ if (templateId) {
+ fetch();
+ }
+ }, [templateId]);
+
+
+ if (!templateId || !state) return null;
+ return (
+
+ setVisible(!visible)}>{visible ? t('reportcenter.labels.advanced_filters_hide') : t('reportcenter.labels.advanced_filters_show')}
+
+ {visible && (
+
+ {state.filters && state.filters.length > 0 && (
+
+
+ {(fields, {add, remove, move}) => {
+ return (
+
+ {fields.map((field, index) => (
+
+
+
+
+ {
+ return {
+ value: f.name,
+ label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
+ }
+ })
+ : []
+ }
+ />
+
+
+
+
+ {
+ () => {
+ const name = form.getFieldValue(['filters', field.name, "field"]);
+ const type = state.filters.find(f => f.name === name)?.type;
+
+ return
+
+
+ }
+ }
+
+
+
+
+
+ {
+ () => {
+ const name = form.getFieldValue(['filters', field.name, "field"]);
+ const type = state.filters.find(f => f.name === name)?.type;
+
+ return
+ {type === 'number' ?
+ {
+ form.setFieldsValue({[field.name]: {value: parseInt(value)}});
+ }}
+ />
+ :
+ {
+ form.setFieldsValue({[field.name]: {value: value.toString()}});
+ }}
+ />
+ }
+
+ }
+ }
+
+
+
+
+ {
+ remove(field.name);
+ }}
+ />
+
+
+
+ ))}
+
+ {
+ add();
+ }}
+ style={{width: "100%"}}
+ >
+ {t("general.actions.add")}
+
+
+
+ );
+ }}
+
+
+
+ )}
+ {state.sorters && state.sorters.length > 0 && (
+
+
+ {(fields, {add, remove, move}) => {
+ return (
+
+ Sorters
+ {fields.map((field, index) => (
+
+
+
+
+ ({
+ value: f.name,
+ label: t(f.translation),
+ }))
+ : []
+ }
+ />
+
+
+
+
+
+
+
+
+
+ {
+ remove(field.name);
+ }}
+ />
+
+
+
+ ))}
+
+ {
+ add();
+ }}
+ style={{width: "100%"}}
+ >
+ {t("general.actions.add")}
+
+
+
+ );
+ }}
+
+
+ )}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx
index c4bef11c3..af4ce7ef4 100644
--- a/client/src/components/report-center-modal/report-center-modal.component.jsx
+++ b/client/src/components/report-center-modal/report-center-modal.component.jsx
@@ -1,30 +1,22 @@
-import { useLazyQuery } from "@apollo/client";
-import {
- Button,
- Card,
- Col,
- DatePicker,
- Form,
- Input,
- Radio,
- Row,
- Typography,
-} from "antd";
+import {useLazyQuery} from "@apollo/client";
+import {Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography,} from "antd";
import _ from "lodash";
import moment from "moment";
-import React, { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { connect } from "react-redux";
-import { createStructuredSelector } from "reselect";
-import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries";
-import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
-import { selectReportCenter } from "../../redux/modals/modals.selectors";
-import DatePIckerRanges from "../../utils/DatePickerRanges";
-import { GenerateDocument } from "../../utils/RenderTemplate";
-import { TemplateList } from "../../utils/TemplateConstants";
+import React, {useState} from "react";
+import {useTranslation} from "react-i18next";
+import {connect} from "react-redux";
+import {createStructuredSelector} from "reselect";
+import {QUERY_ACTIVE_EMPLOYEES} from "../../graphql/employees.queries";
+import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries";
+import {selectReportCenter} from "../../redux/modals/modals.selectors";
+import DatePickerRanges from "../../utils/DatePickerRanges";
+import {GenerateDocument} from "../../utils/RenderTemplate";
+import {TemplateList} from "../../utils/TemplateConstants";
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import "./report-center-modal.styles.scss";
+import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
+
const mapStateToProps = createStructuredSelector({
reportCenterModal: selectReportCenter,
});
@@ -32,39 +24,39 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
- mapStateToProps,
- mapDispatchToProps
+ mapStateToProps,
+ mapDispatchToProps
)(ReportCenterModalComponent);
-export function ReportCenterModalComponent({ reportCenterModal }) {
+export function ReportCenterModalComponent({reportCenterModal}) {
const [form] = Form.useForm();
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
- const { t } = useTranslation();
+ const {t} = useTranslation();
const Templates = TemplateList("report_center");
const ReportsList = Object.keys(Templates).map((key) => {
return Templates[key];
});
- const { visible } = reportCenterModal;
+ const {open} = reportCenterModal;
- const [callVendorQuery, { data: vendorData, called: vendorCalled }] =
- useLazyQuery(QUERY_ALL_VENDORS, {
- skip: !(
- visible &&
- Templates[form.getFieldValue("key")] &&
- Templates[form.getFieldValue("key")].idtype
- ),
- });
+ const [callVendorQuery, {data: vendorData, called: vendorCalled}] =
+ useLazyQuery(QUERY_ALL_VENDORS, {
+ skip: !(
+ open &&
+ Templates[form.getFieldValue("key")] &&
+ Templates[form.getFieldValue("key")].idtype
+ ),
+ });
- const [callEmployeeQuery, { data: employeeData, called: employeeCalled }] =
- useLazyQuery(QUERY_ACTIVE_EMPLOYEES, {
- skip: !(
- visible &&
- Templates[form.getFieldValue("key")] &&
- Templates[form.getFieldValue("key")].idtype
- ),
- });
+ const [callEmployeeQuery, {data: employeeData, called: employeeCalled}] =
+ useLazyQuery(QUERY_ACTIVE_EMPLOYEES, {
+ skip: !(
+ open &&
+ Templates[form.getFieldValue("key")] &&
+ Templates[form.getFieldValue("key")].idtype
+ ),
+ });
const handleFinish = async (values) => {
setLoading(true);
@@ -73,243 +65,245 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
const { id } = values;
await GenerateDocument(
- {
- name: values.key,
- variables: {
- ...(start
- ? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
- : {}),
- ...(end
- ? { end: moment(end).endOf("day").format("YYYY-MM-DD") }
- : {}),
- ...(start ? { starttz: moment(start).startOf("day") } : {}),
- ...(end ? { endtz: moment(end).endOf("day") } : {}),
+ {
+ name: values.key,
+ variables: {
+ ...(start
+ ? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
+ : {}),
+ ...(end ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } : {}),
+ ...(start ? { starttz: moment(start).startOf("day") } : {}),
+ ...(end ? { endtz: moment(end).endOf("day") } : {}),
- ...(id ? { id: id } : {}),
+ ...(id ? { id: id } : {}),
+ },
+ filters: values.filters,
+ sorters: values.sorters,
},
- },
- {
- to: values.to,
- subject: Templates[values.key]?.subject,
- },
- values.sendbyexcel === "excel"
- ? "x"
- : values.sendby === "email"
- ? "e"
- : "p",
- id
+ {
+ to: values.to,
+ subject: Templates[values.key]?.subject,
+ },
+ values.sendbyexcel === "excel"
+ ? "x"
+ : values.sendby === "email"
+ ? "e"
+ : "p",
+ id
);
setLoading(false);
};
const FilteredReportsList =
- search !== ""
- ? ReportsList.filter((r) =>
- r.title.toLowerCase().includes(search.toLowerCase())
- )
- : ReportsList;
+ search !== ""
+ ? ReportsList.filter((r) =>
+ r.title.toLowerCase().includes(search.toLowerCase())
+ )
+ : ReportsList;
//Group it, create cards, and then filter out.
const grouped = _.groupBy(FilteredReportsList, "group");
return (
-
+
+ form.submit()} style={{}} loading={loading}>
+ {t("reportcenter.actions.generate")}
+
+
+
+
);
}
+
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index af575757c..aeeb7f89c 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -2572,6 +2572,11 @@
"generate": "Generate"
},
"labels": {
+ "advanced_filters": "Advanced Filters and Sorters",
+ "advanced_filters_show": "Show",
+ "advanced_filters_hide": "Hide",
+ "advanced_filters_filters": "Filters",
+ "advanced_filters_sorters": "Sorters",
"dates": "Dates",
"employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 5e57e0911..4310e822a 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -2572,6 +2572,11 @@
"generate": ""
},
"labels": {
+ "advanced_filters": "",
+ "advanced_filters_show": "",
+ "advanced_filters_hide": "",
+ "advanced_filters_filters": "",
+ "advanced_filters_sorters": "",
"dates": "",
"employee": "",
"filterson": "",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 92616ae1c..b898ac1f4 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -2572,6 +2572,11 @@
"generate": ""
},
"labels": {
+ "advanced_filters": "",
+ "advanced_filters_show": "",
+ "advanced_filters_hide": "",
+ "advanced_filters_filters": "",
+ "advanced_filters_sorters": "",
"dates": "",
"employee": "",
"filterson": "",
diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js
index 8cb4691fe..9c14eb31c 100644
--- a/client/src/utils/RenderTemplate.js
+++ b/client/src/utils/RenderTemplate.js
@@ -1,14 +1,16 @@
-import { gql } from "@apollo/client";
+import {gql} from "@apollo/client";
import jsreport from "@jsreport/browser-client";
-import { notification } from "antd";
+import {notification} from "antd";
import axios from "axios";
import _ from "lodash";
-import { auth } from "../firebase/firebase.utils";
-import { setEmailOptions } from "../redux/email/email.actions";
-import { store } from "../redux/store";
+import {auth} from "../firebase/firebase.utils";
+import {setEmailOptions} from "../redux/email/email.actions";
+import {store} from "../redux/store";
import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios";
-import { TemplateList } from "./TemplateConstants";
+import {TemplateList} from "./TemplateConstants";
+import {applyFilters, applySorters, parseQuery, printQuery, wrapFiltersInAnd} from "./graphQLmodifier";
+
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
jsreport.serverUrl = server;
@@ -16,11 +18,11 @@ jsreport.serverUrl = server;
const Templates = TemplateList();
export default async function RenderTemplate(
- templateObject,
- bodyshop,
- renderAsHtml = false,
- renderAsExcel = false,
- renderAsText = false
+ templateObject,
+ bodyshop,
+ renderAsHtml = false,
+ renderAsExcel = false,
+ renderAsText = false
) {
if (window.jsr3) {
jsreport.serverUrl = "https://reports3.test.imex.online/";
@@ -30,41 +32,41 @@ export default async function RenderTemplate(
jsreport.headers["Authorization"] = jsrAuth;
//Query assets that match the template name. Must be in format <>.query
- let { contextData, useShopSpecificTemplate } = await fetchContextData(
- templateObject,
- jsrAuth
+ let {contextData, useShopSpecificTemplate} = await fetchContextData(
+ templateObject,
+ jsrAuth
);
- const { ignoreCustomMargins } = Templates[templateObject.name];
+ const {ignoreCustomMargins} = Templates[templateObject.name];
let reportRequest = {
template: {
name: useShopSpecificTemplate
- ? `/${bodyshop.imexshopid}/${templateObject.name}`
- : `/${templateObject.name}`,
+ ? `/${bodyshop.imexshopid}/${templateObject.name}`
+ : `/${templateObject.name}`,
...(renderAsHtml
- ? {}
- : {
+ ? {}
+ : {
recipe: "chrome-pdf",
...(!ignoreCustomMargins && {
chrome: {
marginTop:
- bodyshop.logo_img_path &&
- bodyshop.logo_img_path.headerMargin &&
- bodyshop.logo_img_path.headerMargin > 36
- ? bodyshop.logo_img_path.headerMargin
- : "36px",
+ bodyshop.logo_img_path &&
+ bodyshop.logo_img_path.headerMargin &&
+ bodyshop.logo_img_path.headerMargin > 36
+ ? bodyshop.logo_img_path.headerMargin
+ : "36px",
marginBottom:
- bodyshop.logo_img_path &&
- bodyshop.logo_img_path.footerMargin &&
- bodyshop.logo_img_path.footerMargin > 50
- ? bodyshop.logo_img_path.footerMargin
- : "50px",
+ bodyshop.logo_img_path &&
+ bodyshop.logo_img_path.footerMargin &&
+ bodyshop.logo_img_path.footerMargin > 50
+ ? bodyshop.logo_img_path.footerMargin
+ : "50px",
},
}),
}),
- ...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
- ...(renderAsText ? { recipe: "text" } : {}),
+ ...(renderAsExcel ? {recipe: "html-to-xlsx"} : {}),
+ ...(renderAsText ? {recipe: "text"} : {}),
},
data: {
...contextData,
@@ -73,7 +75,7 @@ export default async function RenderTemplate(
headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop,
- offset: bodyshop.timezone, //moment().utcOffset(),
+ offset: bodyshop.timezone, //dayjs().utcOffset(),
},
};
@@ -82,8 +84,8 @@ export default async function RenderTemplate(
if (!renderAsHtml) {
render.download(
- (Templates[templateObject.name] &&
- Templates[templateObject.name].title) ||
+ (Templates[templateObject.name] &&
+ Templates[templateObject.name].title) ||
""
);
} else {
@@ -97,17 +99,17 @@ export default async function RenderTemplate(
...(!ignoreCustomMargins && {
chrome: {
marginTop:
- bodyshop.logo_img_path &&
- bodyshop.logo_img_path.headerMargin &&
- bodyshop.logo_img_path.headerMargin > 36
- ? bodyshop.logo_img_path.headerMargin
- : "36px",
+ bodyshop.logo_img_path &&
+ bodyshop.logo_img_path.headerMargin &&
+ bodyshop.logo_img_path.headerMargin > 36
+ ? bodyshop.logo_img_path.headerMargin
+ : "36px",
marginBottom:
- bodyshop.logo_img_path &&
- bodyshop.logo_img_path.footerMargin &&
- bodyshop.logo_img_path.footerMargin > 50
- ? bodyshop.logo_img_path.footerMargin
- : "50px",
+ bodyshop.logo_img_path &&
+ bodyshop.logo_img_path.footerMargin &&
+ bodyshop.logo_img_path.footerMargin > 50
+ ? bodyshop.logo_img_path.footerMargin
+ : "50px",
},
}),
},
@@ -121,21 +123,21 @@ export default async function RenderTemplate(
resolve({
pdf,
filename:
- Templates[templateObject.name] &&
- Templates[templateObject.name].title,
+ Templates[templateObject.name] &&
+ Templates[templateObject.name].title,
html,
});
});
}
} catch (error) {
- notification["error"]({ message: JSON.stringify(error) });
+ notification["error"]({message: JSON.stringify(error)});
}
}
export async function RenderTemplates(
- templateObjects,
- bodyshop,
- renderAsHtml = false
+ templateObjects,
+ bodyshop,
+ renderAsHtml = false
) {
//Query assets that match the template name. Must be in format <>.query
let unsortedTemplatesAndData = [];
@@ -145,17 +147,18 @@ export async function RenderTemplates(
templateObjects.forEach((template) => {
proms.push(
- (async () => {
- let { contextData, useShopSpecificTemplate } = await fetchContextData(
- template,
- jsrAuth
- );
- unsortedTemplatesAndData.push({
- templateObject: template,
- contextData,
- useShopSpecificTemplate,
- });
- })()
+ (async () => {
+ console.log(' RENDER TEMPLATE 2 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
+ let {contextData, useShopSpecificTemplate} = await fetchContextData(
+ template,
+ jsrAuth
+ );
+ unsortedTemplatesAndData.push({
+ templateObject: template,
+ contextData,
+ useShopSpecificTemplate,
+ });
+ })()
);
});
await Promise.all(proms);
@@ -172,8 +175,8 @@ export async function RenderTemplates(
unsortedTemplatesAndData.sort(function (a, b) {
return (
- templateObjects.findIndex((x) => x.name === a.templateObject.name) -
- templateObjects.findIndex((x) => x.name === b.templateObject.name)
+ templateObjects.findIndex((x) => x.name === a.templateObject.name) -
+ templateObjects.findIndex((x) => x.name === b.templateObject.name)
);
});
const templateAndData = unsortedTemplatesAndData;
@@ -183,25 +186,25 @@ export async function RenderTemplates(
let reportRequest = {
template: {
name: rootTemplate.useShopSpecificTemplate
- ? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
- : `/${rootTemplate.templateObject.name}`,
+ ? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}`
+ : `/${rootTemplate.templateObject.name}`,
...(renderAsHtml
- ? {}
- : {
+ ? {}
+ : {
recipe: "chrome-pdf",
chrome: {
marginTop:
- bodyshop.logo_img_path &&
- bodyshop.logo_img_path.headerMargin &&
- bodyshop.logo_img_path.headerMargin > 36
- ? bodyshop.logo_img_path.headerMargin
- : "36px",
+ bodyshop.logo_img_path &&
+ bodyshop.logo_img_path.headerMargin &&
+ bodyshop.logo_img_path.headerMargin > 36
+ ? bodyshop.logo_img_path.headerMargin
+ : "36px",
marginBottom:
- bodyshop.logo_img_path &&
- bodyshop.logo_img_path.footerMargin &&
- bodyshop.logo_img_path.footerMargin > 50
- ? bodyshop.logo_img_path.footerMargin
- : "50px",
+ bodyshop.logo_img_path &&
+ bodyshop.logo_img_path.footerMargin &&
+ bodyshop.logo_img_path.footerMargin > 50
+ ? bodyshop.logo_img_path.footerMargin
+ : "50px",
},
}),
pdfOperations: [
@@ -218,22 +221,22 @@ export async function RenderTemplates(
template: {
chrome: {
marginTop:
- bodyshop.logo_img_path &&
- bodyshop.logo_img_path.headerMargin &&
- bodyshop.logo_img_path.headerMargin > 36
- ? bodyshop.logo_img_path.headerMargin
- : "36px",
+ bodyshop.logo_img_path &&
+ bodyshop.logo_img_path.headerMargin &&
+ bodyshop.logo_img_path.headerMargin > 36
+ ? bodyshop.logo_img_path.headerMargin
+ : "36px",
marginBottom:
- bodyshop.logo_img_path &&
- bodyshop.logo_img_path.footerMargin &&
- bodyshop.logo_img_path.footerMargin > 50
- ? bodyshop.logo_img_path.footerMargin
- : "50px",
+ bodyshop.logo_img_path &&
+ bodyshop.logo_img_path.footerMargin &&
+ bodyshop.logo_img_path.footerMargin > 50
+ ? bodyshop.logo_img_path.footerMargin
+ : "50px",
},
name: template.useShopSpecificTemplate
- ? `/${bodyshop.imexshopid}/${template.templateObject.name}`
- : `/${template.templateObject.name}`,
- ...(renderAsHtml ? {} : { recipe: "chrome-pdf" }),
+ ? `/${bodyshop.imexshopid}/${template.templateObject.name}`
+ : `/${template.templateObject.name}`,
+ ...(renderAsHtml ? {} : {recipe: "chrome-pdf"}),
},
type: "append",
@@ -245,8 +248,8 @@ export async function RenderTemplates(
},
data: {
...extend(
- rootTemplate.contextData,
- ...templateAndData.map((temp) => temp.contextData)
+ rootTemplate.contextData,
+ ...templateAndData.map((temp) => temp.contextData)
),
// ...rootTemplate.templateObject.variables,
@@ -266,29 +269,29 @@ export async function RenderTemplates(
return render.toString();
}
} catch (error) {
- notification["error"]({ message: JSON.stringify(error) });
+ notification["error"]({message: JSON.stringify(error)});
}
}
export const GenerateDocument = async (
- template,
- messageOptions,
- sendType,
- jobid
+ template,
+ messageOptions,
+ sendType,
+ jobid
) => {
const bodyshop = store.getState().user.bodyshop;
if (sendType === "e") {
store.dispatch(
- setEmailOptions({
- jobid,
- messageOptions: {
- ...messageOptions,
- to: Array.isArray(messageOptions.to)
- ? messageOptions.to
- : [messageOptions.to],
- },
- template,
- })
+ setEmailOptions({
+ jobid,
+ messageOptions: {
+ ...messageOptions,
+ to: Array.isArray(messageOptions.to)
+ ? messageOptions.to
+ : [messageOptions.to],
+ },
+ template,
+ })
);
} else if (sendType === "x") {
console.log("excel");
@@ -305,22 +308,75 @@ export const GenerateDocuments = async (templates) => {
await RenderTemplates(templates, bodyshop);
};
+export const fetchFilterData = async ({name}) => {
+ try {
+ const bodyshop = store.getState().user.bodyshop;
+ const jsrAuth = (await axios.post("/utils/jsr")).data;
+ jsreport.headers["FirebaseAuthorization"] =
+ "Bearer " + (await auth.currentUser.getIdToken());
+
+ const folders = await cleanAxios.get(`${server}/odata/folders`, {
+ headers: {Authorization: jsrAuth},
+ });
+ const shopSpecificFolder = folders.data.value.find(
+ (f) => f.name === bodyshop.imexshopid
+ );
+
+ const jsReportFilters = await cleanAxios.get(
+ `${server}/odata/assets?$filter=name eq '${name}.filters'`,
+ {headers: {Authorization: jsrAuth}}
+ );
+ console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
+
+ let parsedFilterData;
+ let useShopSpecificTemplate = false;
+ // let shopSpecificTemplate;
+
+ if (shopSpecificFolder) {
+ let shopSpecificTemplate = jsReportFilters.data.value.find(
+ (f) => f?.folder?.shortid === shopSpecificFolder.shortid
+ );
+ if (shopSpecificTemplate) {
+ useShopSpecificTemplate = true;
+ parsedFilterData = atob(shopSpecificTemplate.content);
+ }
+ }
+
+ if (!parsedFilterData) {
+ const generalTemplate = jsReportFilters.data.value.find((f) => !f.folder);
+ useShopSpecificTemplate = false;
+ if (generalTemplate) parsedFilterData = atob(generalTemplate.content);
+ }
+ const data = JSON.parse(parsedFilterData);
+ return {
+ data,
+ useShopSpecificTemplate,
+ success: true,
+ }
+ } catch {
+ return {
+ success: false,
+ }
+ }
+};
+
const fetchContextData = async (templateObject, jsrAuth) => {
+ console.log(' FETCH CONTEXT DATA !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
const bodyshop = store.getState().user.bodyshop;
jsreport.headers["FirebaseAuthorization"] =
- "Bearer " + (await auth.currentUser.getIdToken());
+ "Bearer " + (await auth.currentUser.getIdToken());
const folders = await cleanAxios.get(`${server}/odata/folders`, {
- headers: { Authorization: jsrAuth },
+ headers: {Authorization: jsrAuth},
});
const shopSpecificFolder = folders.data.value.find(
- (f) => f.name === bodyshop.imexshopid
+ (f) => f.name === bodyshop.imexshopid
);
const jsReportQueries = await cleanAxios.get(
- `${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
- { headers: { Authorization: jsrAuth } }
+ `${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`,
+ {headers: {Authorization: jsrAuth}}
);
let templateQueryToExecute;
@@ -329,7 +385,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
if (shopSpecificFolder) {
let shopSpecificTemplate = jsReportQueries.data.value.find(
- (f) => f?.folder?.shortid === shopSpecificFolder.shortid
+ (f) => f?.folder?.shortid === shopSpecificFolder.shortid
);
if (shopSpecificTemplate) {
useShopSpecificTemplate = true;
@@ -343,16 +399,58 @@ const fetchContextData = async (templateObject, jsrAuth) => {
templateQueryToExecute = atob(generalTemplate.content);
}
+ 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)) {
+ console.log('No filters or sorters');
+ 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) {
+ console.log('Applying filters')
+ applyFilters(ast, templateObject.filters, filterFields);
+ wrapFiltersInAnd(ast, filterFields);
+ }
+
+ if (templateObject?.sorters && templateObject?.sorters?.length) {
+ console.log('Applying sorters')
+ applySorters(ast, templateObject.sorters);
+ }
+
+ const finalQuery = printQuery(ast);
+
+ console.log('Modified Query');
+ console.log(finalQuery);
+
let contextData = {};
if (templateQueryToExecute) {
- const { data } = await client.query({
- query: gql(templateQueryToExecute),
- variables: { ...templateObject.variables },
+ const {data} = await client.query({
+ query: gql(finalQuery),
+ variables: {...templateObject.variables},
});
contextData = data;
}
- return { contextData, useShopSpecificTemplate };
+ return {contextData, useShopSpecificTemplate};
};
//export const displayTemplateInWindow = (html) => {
@@ -389,7 +487,7 @@ const fetchContextData = async (templateObject, jsrAuth) => {
function extend(o1, o2, o3) {
var result = {},
- obj;
+ obj;
for (var i = 0; i < arguments.length; i++) {
obj = arguments[i];
@@ -405,4 +503,4 @@ function extend(o1, o2, o3) {
}
}
return result;
-}
+}
\ No newline at end of file
diff --git a/client/src/utils/graphQLmodifier.js b/client/src/utils/graphQLmodifier.js
new file mode 100644
index 000000000..5716753ad
--- /dev/null
+++ b/client/src/utils/graphQLmodifier.js
@@ -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 */
\ No newline at end of file