diff --git a/.circleci/config.yml b/.circleci/config.yml
index f6268a2ae..1b9f64162 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -96,7 +96,7 @@ jobs:
rome-app-build:
docker:
- image: cimg/node:16.15.0
-
+ resource_class: large
working_directory: ~/repo/client
steps:
@@ -136,7 +136,7 @@ jobs:
test-rome-app-build:
docker:
- image: cimg/node:16.15.0
-
+ resource_class: large
working_directory: ~/repo/client
steps:
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 000000000..a2bd52a34
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1,16 @@
+exports.default = {
+ printWidth: 120,
+ useTabs: false,
+ tabWidth: 2,
+ trailingComma: "es5",
+ semi: true,
+ singleQuote: false,
+ bracketSpacing: true,
+ arrowParens: "always",
+ jsxSingleQuote: false,
+ bracketSameLine: false,
+ endOfLine: "lf",
+ importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
+ importOrderSeparation: true,
+ importOrderSortSpecifiers: true,
+};
diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md
index bcaa08ade..2aea3f11a 100644
--- a/_reference/reportFiltersAndSorters.md
+++ b/_reference/reportFiltersAndSorters.md
@@ -3,6 +3,15 @@
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.
+For filters and sorters, valid types include (`type` key in the schema):
+- string (default)
+- number
+- bool or boolean
+- date
+
+## Special Notes
+- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
+
## High level Schema Overview
```javascript
@@ -36,6 +45,38 @@ const schema = {
Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
A note on special notation used in the `name` field.
+## Reflection
+
+Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
+
+```json
+ {
+ "name": "jobs.status",
+ "translation": "jobs.fields.status",
+ "label": "Status",
+ "type": "string",
+ "reflector": {
+ "type": "internal",
+ "name": "special.job_statuses"
+ }
+ },
+```
+
+in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses`
+
+The following cases are available
+
+- `special.job_statuses` - This will reflect the statuses of the jobs table `bodyshop.md_ro_statuses.statuses'`
+- `special.cost_centers` - This will reflect the cost centers `bodyshop.md_responsibility_centers.costs`
+- `special.categories` - This will reflect the categories `bodyshop.md_categories`
+- `special.insurance_companies` - This will reflect the insurance companies `bodyshop.md_ins_cos`'
+- `special.employee_teams` - This will reflect the employee teams `bodyshop.employee_teams`
+- `special.employees` - This will reflect the employees `bodyshop.employees`
+- `special.first_names` - This will reflect the first names `bodyshop.employees`
+- `special.last_names` - This will reflect the last names `bodyshop.employees`
+- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources
+- `special.class`- This will reflect the class `bodyshop.md_classes`
+-
### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",`
@@ -71,8 +112,8 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
}
```
-
### Path with brackets,top level
+
`"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query.
@@ -107,14 +148,37 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
```
## 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.
+
+- 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 type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' 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 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.
+
+- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting,
+ a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs`
+ would be added at the `joblines` level.
+- Most of the reports currently do sorting on a template level, this will need to change to actually see the results
+ using the sorters.
+
+### Default Sorters
+
+- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
+- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.
+
+```json
+{
+ "name": "jobs.joblines.mod_lb_hrs",
+ "translation": "jobs.joblines.mod_lb_hrs_1",
+ "label": "mod_lb_hrs_1",
+ "type": "number",
+ "default": {
+ "order": 1,
+ "direction": "asc"
+ }
+}
+```
diff --git a/client/src/components/bill-delete-button/bill-delete-button.component.jsx b/client/src/components/bill-delete-button/bill-delete-button.component.jsx
index 5d2d154f6..ca3f3822d 100644
--- a/client/src/components/bill-delete-button/bill-delete-button.component.jsx
+++ b/client/src/components/bill-delete-button/bill-delete-button.component.jsx
@@ -3,10 +3,22 @@ import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
import { DELETE_BILL } from "../../graphql/bills.queries";
+import { insertAuditTrail } from "../../redux/application/application.actions";
+import AuditTrailMapping from "../../utils/AuditTrailMappings";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
-export default function BillDeleteButton({ bill, callback }) {
+const mapStateToProps = createStructuredSelector({});
+const mapDispatchToProps = (dispatch) => ({
+ insertAuditTrail: ({ jobid, operation }) =>
+ dispatch(insertAuditTrail({ jobid, operation })),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
+
+export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL);
@@ -36,6 +48,10 @@ export default function BillDeleteButton({ bill, callback }) {
if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") });
+ insertAuditTrail({
+ jobid: jobid,
+ operation: AuditTrailMapping.billdeleted(bill.invoice_number),
+ });
if (callback && typeof callback === "function") callback(bill.id);
} else {
diff --git a/client/src/components/bills-list-table/bills-list-table.component.jsx b/client/src/components/bills-list-table/bills-list-table.component.jsx
index 9dea48f71..5f5bd7011 100644
--- a/client/src/components/bills-list-table/bills-list-table.component.jsx
+++ b/client/src/components/bills-list-table/bills-list-table.component.jsx
@@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
-import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
+import { alphaSort, dateSort } from "../../utils/sorters";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -58,7 +58,7 @@ export function BillsListTableComponent({
)}
-
+
)}
+ {create && (
+
p.scheduledreturn !== c.scheduledreturn}
+ >
+ {() => {
+ const insuranceOver =
+ selectedCar &&
+ selectedCar.insuranceexpires &&
+ moment(selectedCar.insuranceexpires)
+ .endOf("day")
+ .isBefore(moment(form.getFieldValue("scheduledreturn")));
+ if (insuranceOver)
+ return (
+
+
+
+ {t("contracts.labels.insuranceexpired")}
+
+
+ );
+ return <>>;
+ }}
+
+ )}
{() => {
const mileageOver =
- selectedCar &&
- selectedCar.nextservicekm <= form.getFieldValue("kmstart");
-
+ selectedCar && selectedCar.nextservicekm
+ ? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
+ : false;
const dueForService =
selectedCar &&
selectedCar.nextservicedate &&
- moment(selectedCar.nextservicedate).isBefore(
- moment(form.getFieldValue("scheduledreturn"))
- );
-
+ moment(selectedCar.nextservicedate)
+ .endOf("day")
+ .isSameOrBefore(
+ moment(form.getFieldValue("scheduledreturn"))
+ );
if (mileageOver || dueForService)
return (
@@ -117,7 +142,6 @@ export default function ContractFormComponent({
);
-
return <>>;
}}
diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx
index 2c992990c..3bd49d6e7 100644
--- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx
+++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx
@@ -17,13 +17,18 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
+import useLocalStorage from "../../utils/useLocalStorage";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
+
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
- filteredInfo: { text: "" },
});
const [searchText, setSearchText] = useState("");
+ const [filter, setFilter] = useLocalStorage(
+ "filter_courtesy_cars_list",
+ null
+ );
const { t } = useTranslation();
const columns = [
@@ -50,6 +55,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
+ filteredValue: filter?.status || null,
filters: [
{
text: t("courtesycars.status.in"),
@@ -72,7 +78,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
- const { nextservicedate, nextservicekm, mileage } = record;
+ const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
+ record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
@@ -80,11 +87,25 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
+ const insuranceOver =
+ insuranceexpires &&
+ moment(insuranceexpires).endOf("day").isBefore(moment());
+
return (
{t(record.status)}
- {(mileageOver || dueForService) && (
-
+ {(mileageOver || dueForService || insuranceOver) && (
+
)}
@@ -97,6 +118,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "readiness",
key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
+ filteredValue: filter?.readiness || null,
filters: [
{
text: t("courtesycars.readiness.ready"),
@@ -212,7 +234,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
];
const handleTableChange = (pagination, filters, sorter) => {
- setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ setState({ ...state, sortedInfo: sorter });
+ setFilter(filters);
};
const tableData = searchText
diff --git a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx
index 74609ca49..fdf083a40 100644
--- a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx
+++ b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx
@@ -3,21 +3,32 @@ import {
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
-import { Card, Space, Table, Tooltip } from "antd";
+import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
+import { TimeFormatter } from "../../../utils/DateFormatter";
+import { onlyUnique } from "../../../utils/arrayHelper";
+import { alphaSort, dateSort } from "../../../utils/sorters";
+import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
-import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
+import OwnerNameDisplay, {
+ OwnerNameDisplayFunction,
+} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
-import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledInToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
+ filteredInfo: {},
});
+ const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage(
+ "isTvModeScheduledIn",
+ false
+ );
+
if (!data) return null;
if (!data.scheduled_in_today)
return ;
@@ -31,6 +42,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no,
jobid: item.job.jobid,
+ joblines_body: item.job.joblines
+ .filter((l) => l.mod_lbr_ty !== "LAR")
+ .reduce((acc, val) => acc + val.mod_lb_hrs, 0),
+ joblines_ref: item.job.joblines
+ .filter((l) => l.mod_lbr_ty === "LAR")
+ .reduce((acc, val) => acc + val.mod_lb_hrs, 0),
ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent,
ownerid: item.job.ownerid,
@@ -49,7 +66,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid,
note: item.note,
- start: moment(item.start).format("hh:mm a"),
+ start: item.start,
title: item.title,
};
appt.push(i);
@@ -59,11 +76,192 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
return new moment(a.start) - new moment(b.start);
});
- const columns = [
+ const tvFontSize = 16;
+ const tvFontWeight = "bold";
+
+ const tvColumns = [
+ {
+ title: t("appointments.fields.time"),
+ dataIndex: "start",
+ key: "start",
+ ellipsis: true,
+ sorter: (a, b) => dateSort(a.start, b.start),
+ sortOrder:
+ state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
+ render: (text, record) => (
+
+ {record.start}
+
+ ),
+ },
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
+ sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
+ sortOrder:
+ state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+ render: (text, record) => (
+ e.stopPropagation()}
+ >
+
+
+ {record.ro_number || t("general.labels.na")}
+ {record.production_vars && record.production_vars.alert ? (
+
+ ) : null}
+ {record.suspended && (
+
+ )}
+ {record.iouparent && (
+
+
+
+ )}
+
+
+
+ ),
+ },
+ {
+ title: t("jobs.fields.owner"),
+ dataIndex: "owner",
+ key: "owner",
+ ellipsis: true,
+ sorter: (a, b) =>
+ alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
+ sortOrder:
+ state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.ownerid ? (
+ e.stopPropagation()}
+ >
+
+
+
+
+ ) : (
+
+
+
+ );
+ },
+ },
+ {
+ title: t("jobs.fields.vehicle"),
+ dataIndex: "vehicle",
+ key: "vehicle",
+ ellipsis: true,
+ sorter: (a, b) =>
+ alphaSort(
+ `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
+ a.v_model_desc || ""
+ }`,
+ `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
+ ),
+ sortOrder:
+ state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.vehicleid ? (
+ e.stopPropagation()}
+ >
+
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
+ record.v_model_desc || ""
+ }`}
+
+
+ ) : (
+ {`${
+ record.v_model_yr || ""
+ } ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
+ );
+ },
+ },
+ {
+ title: t("appointments.fields.alt_transport"),
+ dataIndex: "alt_transport",
+ key: "alt_transport",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
+ sortOrder:
+ state.sortedInfo.columnKey === "alt_transport" &&
+ state.sortedInfo.order,
+ filters:
+ (appt &&
+ appt
+ .map((j) => j.alt_transport)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Alt. Transport",
+ value: [s],
+ };
+ })
+ .sort((a, b) => alphaSort(a.text, b.text))) ||
+ [],
+ onFilter: (value, record) => value.includes(record.alt_transport),
+ render: (text, record) => (
+
+ {record.alt_transport}
+
+ ),
+ },
+ {
+ title: t("jobs.fields.lab"),
+ dataIndex: "joblines_body",
+ key: "joblines_body",
+ sorter: (a, b) => a.joblines_body - b.joblines_body,
+ sortOrder:
+ state.sortedInfo.columnKey === "joblines_body" &&
+ state.sortedInfo.order,
+ align: "right",
+ render: (text, record) => (
+
+ {record.joblines_body.toFixed(1)}
+
+ ),
+ },
+ {
+ title: t("jobs.fields.lar"),
+ dataIndex: "joblines_ref",
+ key: "joblines_ref",
+ sorter: (a, b) => a.joblines_ref - b.joblines_ref,
+ sortOrder:
+ state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
+ align: "right",
+ render: (text, record) => (
+
+ {record.joblines_ref.toFixed(1)}
+
+ ),
+ },
+ ];
+
+ const columns = [
+ {
+ title: t("appointments.fields.time"),
+ dataIndex: "start",
+ key: "start",
+ ellipsis: true,
+ sorter: (a, b) => dateSort(a.start, b.start),
+ sortOrder:
+ state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
+ render: (text, record) => {record.start} ,
+ },
+ {
+ title: t("jobs.fields.ro_number"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
+ sortOrder:
+ state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
+ alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
+ sortOrder:
+ state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
(
-
- ),
- },
- {
- title: t("jobs.fields.ownr_ph2"),
- dataIndex: "ownr_ph2",
- key: "ownr_ph2",
- ellipsis: true,
- responsive: ["md"],
- render: (text, record) => (
-
+
+
+
+
),
},
{
@@ -134,7 +328,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
-
+ {record.ownr_ea}
),
},
{
@@ -142,6 +336,15 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
+ sorter: (a, b) =>
+ alphaSort(
+ `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
+ a.v_model_desc || ""
+ }`,
+ `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
+ ),
+ sortOrder:
+ state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
alphaSort(a.ins_co_nm, b.ins_co_nm),
+ sortOrder:
+ state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
+ filters:
+ (appt &&
+ appt
+ .map((j) => j.ins_co_nm)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Ins. Co.*",
+ value: [s],
+ };
+ })
+ .sort((a, b) => alphaSort(a.text, b.text))) ||
+ [],
+ onFilter: (value, record) => value.includes(record.ins_co_nm),
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
- responsive: ["md"],
+ sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
+ sortOrder:
+ state.sortedInfo.columnKey === "alt_transport" &&
+ state.sortedInfo.order,
+ filters:
+ (appt &&
+ appt
+ .map((j) => j.alt_transport)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Alt. Transport",
+ value: [s],
+ };
+ })
+ .sort((a, b) => alphaSort(a.text, b.text))) ||
+ [],
+ onFilter: (value, record) => value.includes(record.alt_transport),
},
];
- const handleTableChange = (sorter) => {
- setState({ ...state, sortedInfo: sorter });
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
+ {t("general.labels.tvmode")}
+ setIsTvModeScheduledIn(!isTvModeScheduledIn)}
+ defaultChecked={isTvModeScheduledIn}
+ />
+
+ }
{...cardProps}
>
@@ -220,6 +460,10 @@ export const DashboardScheduledInTodayGql = `
alt_transport
clm_no
jobid: id
+ joblines(where: {removed: {_eq: false}}) {
+ mod_lb_hrs
+ mod_lbr_ty
+ }
ins_co_nm
iouparent
ownerid
diff --git a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx
index 0407e3aad..c5ac05f58 100644
--- a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx
+++ b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx
@@ -3,37 +3,272 @@ import {
ExclamationCircleFilled,
PauseCircleOutlined,
} from "@ant-design/icons";
-import { Card, Space, Table, Tooltip } from "antd";
+import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
+import { TimeFormatter } from "../../../utils/DateFormatter";
+import { onlyUnique } from "../../../utils/arrayHelper";
+import { alphaSort, dateSort } from "../../../utils/sorters";
+import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
-import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component";
+import OwnerNameDisplay, {
+ OwnerNameDisplayFunction,
+} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component";
-import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledOutToday({ data, ...cardProps }) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
+ filteredInfo: {},
});
+ const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage(
+ "isTvModeScheduledOut",
+ false
+ );
+
if (!data) return null;
if (!data.scheduled_out_today)
return ;
data.scheduled_out_today.forEach((item) => {
- item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
+ item.joblines_body = item.joblines
+ ? item.joblines
+ .filter((l) => l.mod_lbr_ty !== "LAR")
+ .reduce((acc, val) => acc + val.mod_lb_hrs, 0)
+ : 0;
+ item.joblines_ref = item.joblines
+ ? item.joblines
+ .filter((l) => l.mod_lbr_ty === "LAR")
+ .reduce((acc, val) => acc + val.mod_lb_hrs, 0)
+ : 0;
});
data.scheduled_out_today.sort(function (a, b) {
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
});
- const columns = [
+ const tvFontSize = 18;
+ const tvFontWeight = "bold";
+
+ const tvColumns = [
+ {
+ title: t("jobs.fields.scheduled_completion"),
+ dataIndex: "scheduled_completion",
+ key: "scheduled_completion",
+ ellipsis: true,
+ sorter: (a, b) =>
+ dateSort(a.scheduled_completion, b.scheduled_completion),
+ sortOrder:
+ state.sortedInfo.columnKey === "scheduled_completion" &&
+ state.sortedInfo.order,
+ render: (text, record) => (
+
+ {record.scheduled_completion}
+
+ ),
+ },
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
+ sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
+ sortOrder:
+ state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+ render: (text, record) => (
+ e.stopPropagation()}
+ >
+
+
+ {record.ro_number || t("general.labels.na")}
+ {record.production_vars && record.production_vars.alert ? (
+
+ ) : null}
+ {record.suspended && (
+
+ )}
+ {record.iouparent && (
+
+
+
+ )}
+
+
+
+ ),
+ },
+ {
+ title: t("jobs.fields.owner"),
+ dataIndex: "owner",
+ key: "owner",
+ ellipsis: true,
+ sorter: (a, b) =>
+ alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
+ sortOrder:
+ state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.ownerid ? (
+ e.stopPropagation()}
+ >
+
+
+
+
+ ) : (
+
+
+
+ );
+ },
+ },
+ {
+ title: t("jobs.fields.vehicle"),
+ dataIndex: "vehicle",
+ key: "vehicle",
+ ellipsis: true,
+ sorter: (a, b) =>
+ alphaSort(
+ `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
+ a.v_model_desc || ""
+ }`,
+ `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
+ ),
+ sortOrder:
+ state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.vehicleid ? (
+ e.stopPropagation()}
+ >
+
+ {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
+ record.v_model_desc || ""
+ }`}
+
+
+ ) : (
+ {`${
+ record.v_model_yr || ""
+ } ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
+ );
+ },
+ },
+ {
+ title: t("appointments.fields.alt_transport"),
+ dataIndex: "alt_transport",
+ key: "alt_transport",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
+ sortOrder:
+ state.sortedInfo.columnKey === "alt_transport" &&
+ state.sortedInfo.order,
+ filters:
+ (data.scheduled_out_today &&
+ data.scheduled_out_today
+ .map((j) => j.alt_transport)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Alt. Transport*",
+ value: [s],
+ };
+ })
+ .sort((a, b) => alphaSort(a.text, b.text))) ||
+ [],
+ onFilter: (value, record) => value.includes(record.alt_transport),
+ render: (text, record) => (
+
+ {record.alt_transport}
+
+ ),
+ },
+ {
+ title: t("jobs.fields.status"),
+ dataIndex: "status",
+ key: "status",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
+ sortOrder:
+ state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
+ filters:
+ (data.scheduled_out_today &&
+ data.scheduled_out_today
+ .map((j) => j.status)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Status*",
+ value: [s],
+ };
+ })
+ .sort((a, b) => alphaSort(a.text, b.text))) ||
+ [],
+ onFilter: (value, record) => value.includes(record.status),
+ render: (text, record) => (
+
+ {record.status}
+
+ ),
+ },
+ {
+ title: t("jobs.fields.lab"),
+ dataIndex: "joblines_body",
+ key: "joblines_body",
+ sorter: (a, b) => a.joblines_body - b.joblines_body,
+ sortOrder:
+ state.sortedInfo.columnKey === "joblines_body" &&
+ state.sortedInfo.order,
+ align: "right",
+ render: (text, record) => (
+
+ {record.joblines_body.toFixed(1)}
+
+ ),
+ },
+ {
+ title: t("jobs.fields.lar"),
+ dataIndex: "joblines_ref",
+ key: "joblines_ref",
+ sorter: (a, b) => a.joblines_ref - b.joblines_ref,
+ sortOrder:
+ state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
+ align: "right",
+ render: (text, record) => (
+
+ {record.joblines_ref.toFixed(1)}
+
+ ),
+ },
+ ];
+
+ const columns = [
+ {
+ title: t("jobs.fields.scheduled_completion"),
+ dataIndex: "scheduled_completion",
+ key: "scheduled_completion",
+ ellipsis: true,
+ sorter: (a, b) =>
+ dateSort(a.scheduled_completion, b.scheduled_completion),
+ sortOrder:
+ state.sortedInfo.columnKey === "scheduled_completion" &&
+ state.sortedInfo.order,
+ render: (text, record) => (
+ {record.scheduled_completion}
+ ),
+ },
+ {
+ title: t("jobs.fields.ro_number"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
+ sortOrder:
+ state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
+ alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
+ sortOrder:
+ state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
(
-
- ),
- },
- {
- title: t("jobs.fields.ownr_ph2"),
- dataIndex: "ownr_ph2",
- key: "ownr_ph2",
- ellipsis: true,
- responsive: ["md"],
- render: (text, record) => (
-
+
+
+
+
),
},
{
@@ -104,7 +335,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
-
+ {record.ownr_ea}
),
},
{
@@ -112,6 +343,15 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
+ sorter: (a, b) =>
+ alphaSort(
+ `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
+ a.v_model_desc || ""
+ }`,
+ `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
+ ),
+ sortOrder:
+ state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
alphaSort(a.ins_co_nm, b.ins_co_nm),
+ sortOrder:
+ state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
+ filters:
+ (data.scheduled_out_today &&
+ data.scheduled_out_today
+ .map((j) => j.ins_co_nm)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Ins. Co.*",
+ value: [s],
+ };
+ })
+ .sort((a, b) => alphaSort(a.text, b.text))) ||
+ [],
+ onFilter: (value, record) => value.includes(record.ins_co_nm),
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
- responsive: ["md"],
+ sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
+ sortOrder:
+ state.sortedInfo.columnKey === "alt_transport" &&
+ state.sortedInfo.order,
+ filters:
+ (data.scheduled_out_today &&
+ data.scheduled_out_today
+ .map((j) => j.alt_transport)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Alt. Transport*",
+ value: [s],
+ };
+ })
+ .sort((a, b) => alphaSort(a.text, b.text))) ||
+ [],
+ onFilter: (value, record) => value.includes(record.alt_transport),
},
];
- const handleTableChange = (sorter) => {
- setState({ ...state, sortedInfo: sorter });
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
return (
+ {t("general.labels.tvmode")}
+ setIsTvModeScheduledOut(!isTvModeScheduledOut)}
+ defaultChecked={isTvModeScheduledOut}
+ />
+
+ }
{...cardProps}
>
@@ -188,6 +465,10 @@ export const DashboardScheduledOutTodayGql = `
alt_transport
clm_no
jobid: id
+ joblines(where: {removed: {_eq: false}}) {
+ mod_lb_hrs
+ mod_lbr_ty
+ }
ins_co_nm
iouparent
ownerid
@@ -200,6 +481,7 @@ export const DashboardScheduledOutTodayGql = `
production_vars
ro_number
scheduled_completion
+ status
suspended
v_make_desc
v_model_desc
diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx
index d24dd5641..7012d943b 100644
--- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx
+++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx
@@ -275,26 +275,22 @@ const componentList = {
h: 2,
},
ScheduleInToday: {
- label: i18next.t("dashboard.titles.scheduledintoday", {
- date: moment().startOf("day").format("MM/DD/YYYY"),
- }),
+ label: i18next.t("dashboard.titles.scheduledintoday"),
component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql,
- minW: 10,
+ minW: 6,
minH: 2,
w: 10,
- h: 2,
+ h: 3,
},
ScheduleOutToday: {
- label: i18next.t("dashboard.titles.scheduledouttoday", {
- date: moment().startOf("day").format("MM/DD/YYYY"),
- }),
+ label: i18next.t("dashboard.titles.scheduledouttoday"),
component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql,
- minW: 10,
+ minW: 6,
minH: 2,
w: 10,
- h: 2,
+ h: 3,
},
};
@@ -306,8 +302,7 @@ const createDashboardQuery = (state) => {
.map((item, index) => componentList[item.i].gqlFragment || "")
.join("");
return gql`
- query QUERY_DASHBOARD_DETAILS {
- ${componentBasedAdditions || ""}
+ query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}},
{date_invoiced: {_gte: "${moment()
@@ -317,11 +312,11 @@ const createDashboardQuery = (state) => {
.endOf("month")
.endOf("day")
.toISOString()}"}}]}) {
- id
- ro_number
- date_invoiced
- job_totals
- rate_la1
+ id
+ ro_number
+ date_invoiced
+ job_totals
+ rate_la1
rate_la2
rate_la3
rate_la4
@@ -344,43 +339,42 @@ const createDashboardQuery = (state) => {
rate_mapa
rate_mash
rate_matd
- joblines(where: { removed: { _eq: false } }) {
+ joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
+ }
}
- }
- production_jobs: jobs(where: { inproduction: { _eq: true } }) {
+ production_jobs: jobs(where: { inproduction: { _eq: true } }) {
+ id
+ ro_number
+ ins_co_nm
+ job_totals
+ joblines(where: { removed: { _eq: false } }) {
id
- ro_number
- ins_co_nm
- job_totals
- joblines(where: { removed: { _eq: false } }) {
- id
- mod_lbr_ty
- mod_lb_hrs
- act_price
- part_qty
- part_type
- }
- labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
- aggregate {
- sum {
- mod_lb_hrs
- }
+ mod_lbr_ty
+ mod_lb_hrs
+ act_price
+ part_qty
+ part_type
+ }
+ labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
+ aggregate {
+ sum {
+ mod_lb_hrs
}
}
- larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
- aggregate {
- sum {
- mod_lb_hrs
- }
+ }
+ larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
+ aggregate {
+ sum {
+ mod_lb_hrs
}
}
}
}
- `;
+ }`;
};
diff --git a/client/src/components/dashboard-grid/dashboard-grid.styles.scss b/client/src/components/dashboard-grid/dashboard-grid.styles.scss
index 62a3ae72b..140a1e1f3 100644
--- a/client/src/components/dashboard-grid/dashboard-grid.styles.scss
+++ b/client/src/components/dashboard-grid/dashboard-grid.styles.scss
@@ -128,7 +128,7 @@
height: 100%;
width: 100%;
.ant-card-body {
- height: 80%;
+ height: calc(100% - 2rem);
width: 100%;
// // background-color: red;
// height: 90%;
diff --git a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx
index 8bbda03e1..34acb7994 100644
--- a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx
+++ b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx
@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
+import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
+import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
+const mapDispatchToProps = (dispatch) => ({
+ insertAuditTrail: ({ jobid, operation }) =>
+ dispatch(insertAuditTrail({ jobid, operation })),
+});
+
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
@@ -40,6 +47,7 @@ export function JobsCloseExportButton({
disabled,
setSelectedJobs,
refetch,
+ insertAuditTrail,
}) {
const history = useHistory();
const { t } = useTranslation();
@@ -181,6 +189,10 @@ export function JobsCloseExportButton({
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
+ insertAuditTrail({
+ jobid: jobId,
+ operation: AuditTrailMapping.jobexported(),
+ });
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
);
@@ -192,12 +204,20 @@ export function JobsCloseExportButton({
});
}
}
- if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
+ if (
+ bodyshop.accountingconfig &&
+ bodyshop.accountingconfig.qbo &&
+ successfulTransactions.length > 0
+ ) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
+ insertAuditTrail({
+ jobid: jobId,
+ operation: AuditTrailMapping.jobexported(),
+ });
updateJobCache([
...new Set(
successfulTransactions.map(
@@ -227,4 +247,7 @@ export function JobsCloseExportButton({
);
}
-export default connect(mapStateToProps, null)(JobsCloseExportButton);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(JobsCloseExportButton);
diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx
index e3f78ef69..f6eb94619 100644
--- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx
+++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx
@@ -132,6 +132,12 @@ export function JobsDetailHeaderActions({
},
},
});
+ insertAuditTrail({
+ jobid: job.id,
+ operation: AuditTrailMapping.jobsuspend(
+ !!job.suspended ? !job.suspended : true
+ ),
+ });
};
const statusmenu = (
@@ -561,6 +567,10 @@ export function JobsDetailHeaderActions({
notification["success"]({
message: t("jobs.successes.voided"),
});
+ insertAuditTrail({
+ jobid: job.id,
+ operation: AuditTrailMapping.jobvoid(),
+ });
//go back to jobs list.
history.push(`/manage/`);
} else {
diff --git a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx
index 40c7aa4db..70dbc033a 100644
--- a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx
+++ b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx
@@ -123,11 +123,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{job.cccontracts.length > 0 && (
- {job.cccontracts.map((c) => (
- {`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
+ {job.cccontracts.map((c, index) => (
+
+
+ {`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
+ {index !== job.cccontracts.length - 1 ? "," : null}
+
+
))}
)}
diff --git a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
index 3204d7311..2bb338d97 100644
--- a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
+++ b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx
@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOBS } from "../../graphql/jobs.queries";
+import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
+import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
+const mapDispatchToProps = (dispatch) => ({
+ insertAuditTrail: ({ jobid, operation }) =>
+ dispatch(insertAuditTrail({ jobid, operation })),
+});
+
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
@@ -41,6 +48,7 @@ export function JobsExportAllButton({
loadingCallback,
completedCallback,
refetch,
+ insertAuditTrail,
}) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS);
@@ -177,6 +185,12 @@ export function JobsExportAllButton({
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
+ jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
+ insertAuditTrail({
+ jobid: job.id,
+ operation: AuditTrailMapping.jobexported(),
+ });
+ });
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id
@@ -190,13 +204,17 @@ export function JobsExportAllButton({
});
}
}
- if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
+ if (
+ bodyshop.accountingconfig &&
+ bodyshop.accountingconfig.qbo &&
+ successfulTransactions.length > 0
+ ) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
- updateJobCache([
+ const successfulTransactionsSet = [
...new Set(
successfulTransactions.map(
(st) =>
@@ -207,7 +225,14 @@ export function JobsExportAllButton({
]
)
),
- ]);
+ ];
+ if (successfulTransactionsSet.length > 0) {
+ insertAuditTrail({
+ jobid: successfulTransactionsSet[0],
+ operation: AuditTrailMapping.jobexported(),
+ });
+ }
+ updateJobCache(successfulTransactionsSet);
}
}
})
@@ -225,4 +250,7 @@ export function JobsExportAllButton({
);
}
-export default connect(mapStateToProps, null)(JobsExportAllButton);
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(JobsExportAllButton);
diff --git a/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx b/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx
index a150eee78..57e81cf77 100644
--- a/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx
+++ b/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx
@@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
await insertPayment({
variables: {
paymentInput: {
- amount: -refund_response.data.amount,
- transactionid: payment_response.response.receiptelements.transid,
+ amount: -refund_response?.data?.amount,
+ transactionid: payment_response?.response?.receiptelements?.transid,
payer: record.payer,
type: "Refund",
jobid: payment_response.jobid,
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
index 0d962a29f..892ce67d5 100644
--- 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
@@ -1,52 +1,414 @@
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd";
-import React, {useEffect, useState} from "react";
+import React, {useCallback, useEffect, useMemo, useState} from "react";
import {fetchFilterData} from "../../utils/RenderTemplate";
import {DeleteFilled} from "@ant-design/icons";
import {useTranslation} from "react-i18next";
-import {getOperatorsByType} from "../../utils/graphQLmodifier";
+import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
+import {generateInternalReflections} from "./report-center-modal-utils";
+import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
-export default function ReportCenterModalFiltersSortersComponent({form}) {
+export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
return (
{() => {
const key = form.getFieldValue("key");
- return ;
+ return ;
}}
);
}
-function RenderFilters({templateId, form}) {
+/**
+ * Filters Section
+ * @param filters
+ * @param form
+ * @param bodyshop
+ * @returns {JSX.Element}
+ * @constructor
+ */
+function FiltersSection({filters, form, bodyshop}) {
+ const {t} = useTranslation();
+
+ return (
+
+
+ {(fields, {add, remove}) => {
+ return (
+
+ {fields.map((field, index) => (
+
+
+
+
+ trigger.parentNode}
+ onChange={() => {
+ // Clear related Fields
+ form.setFieldValue(['filters', field.name, 'value'], null);
+ form.setFieldValue(['filters', field.name, 'operator'], null);
+ }}
+ options={
+ filters.map((f) => {
+ return {
+ value: f.name,
+ label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
+ }
+ })
+ }
+ />
+
+
+
+
+ {
+ () => {
+ const name = form.getFieldValue(['filters', field.name, "field"]);
+ const type = filters.find(f => f.name === name)?.type;
+
+ return
+ trigger.parentNode}
+ options={ getWhereOperatorsByType(type)}
+ onChange={() => {
+ // Clear related Fields
+
+ form.setFieldValue(['filters', field.name, 'value'], undefined);
+ }}
+ />
+
+ }
+ }
+
+
+
+
+ {
+ () => {
+ // Because it looks cleaner than inlining.
+ const name = form.getFieldValue(['filters', field.name, "field"]);
+ const type = filters.find(f => f.name === name)?.type;
+ const reflector = filters.find(f => f.name === name)?.reflector;
+ const operator = form.getFieldValue(['filters', field.name, "operator"]);
+ const operatorType = operator ? getWhereOperatorsByType(type).find((o) => o.value === operator)?.type : null;
+
+ return
+ {
+ (() => {
+ const generateReflections = (reflector) => {
+ if (!reflector) return [];
+
+ const {name} = reflector;
+ const path = name?.split('.');
+ const upperPath = path?.[0];
+ const finalPath = path?.slice(1).join('.');
+
+ return generateInternalReflections({
+ bodyshop,
+ upperPath,
+ finalPath
+ });
+ };
+
+ const reflections = reflector ? generateReflections(reflector) : [];
+ const fieldPath = [[field.name, "value"]];
+ // We have reflections so we will use a select box
+ if (reflections.length > 0) {
+ // We have reflections and the operator type is array, so we will use a select box with multiple options
+ if (operatorType === "array") {
+ return (
+ trigger.parentNode}
+ onChange={(value) => {
+ form.setFieldValue(fieldPath, value);
+ }}
+ />
+ );
+ }
+ return (
+ trigger.parentNode}
+ onChange={(value) => {
+ form.setFieldValue(fieldPath, value);
+ }}
+ />
+ );
+ }
+
+ // We have a type of number, so we will use a number input
+ if (type === "number") {
+ return (
+ form.setFieldValue(fieldPath, value)}/>
+ );
+ }
+
+ // We have a type of date, so we will use a date picker
+ if (type === "date") {
+ return (
+ form.setFieldValue(fieldPath, date)}
+ />
+ );
+ }
+
+ // we have a type of boolean, so we will use a select box with a true or false option.
+ if (type === "boolean" || type === "bool") {
+ return (
+ trigger.parentNode}
+ options={[
+ {
+ label: t('reportcenter.labels.advanced_filters_true'),
+ value: true
+ },
+ {
+ label: t('reportcenter.labels.advanced_filters_false'),
+ value: false
+ }
+ ]}
+ onChange={(value) => form.setFieldValue(fieldPath, value)}
+ />
+ );
+ }
+ return (
+ form.setFieldValue(fieldPath, e.target.value)}
+ />
+ );
+ })()
+ }
+
+ }
+ }
+
+
+
+
+ {
+ remove(field.name);
+ }}
+ />
+
+
+
+ ))}
+
+ {
+ add();
+ }}
+ style={{width: "100%"}}
+ >
+ {t("general.actions.add")}
+
+
+
+ );
+ }}
+
+
+ );
+}
+
+/**
+ * Sorters Section
+ * @param sorters
+ * @param form
+ * @returns {JSX.Element}
+ * @constructor
+ */
+function SortersSection({sorters}) {
+ const {t} = useTranslation();
+ return (
+
+
+ {(fields, {add, remove}) => {
+ return (
+
+ Sorters
+ {fields.map((field, index) => (
+
+
+
+
+ ({
+ value: f.name,
+ label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
+ }))
+ }
+ getPopupContainer={trigger => trigger.parentNode}
+ />
+
+
+
+
+ trigger.parentNode}
+ />
+
+
+
+
+ {
+ remove(field.name);
+ }}
+ />
+
+
+
+ ))}
+
+ {
+ add();
+ }}
+ style={{width: "100%"}}
+ >
+ {t("general.actions.add")}
+
+
+
+ );
+ }}
+
+
+ );
+}
+
+/**
+ * Render Filters
+ * @param templateId
+ * @param form
+ * @param bodyshop
+ * @returns {JSX.Element|null}
+ * @constructor
+ */
+function RenderFilters({templateId, form, bodyshop}) {
const [state, setState] = useState(null);
const [visible, setVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation();
- useEffect(() => {
- const fetch = async () => {
- setIsLoading(true);
- const data = await fetchFilterData({name: templateId});
- if (data?.success) {
- setState(data.data);
- } else {
- setState(null);
- }
- setIsLoading(false);
- };
+ const fetch = useCallback(async () => {
+ // Reset all the filters and Sorters.
+ form.resetFields(['filters']);
+ form.resetFields(['sorters']);
+ form.resetFields(['defaultSorters']);
+ setIsLoading(true);
+
+ const data = await fetchFilterData({name: templateId});
+
+ // We have Success
+ if (data?.success) {
+ if (data?.data?.sorters && data?.data?.sorters.length > 0) {
+ const defaultSorters = data?.data?.sorters.filter((sorter) => sorter.hasOwnProperty('default')).map((sorter) => {
+ return {
+ field: sorter.name,
+ direction: sorter.default.direction
+ };
+ }).sort((a, b) => a.default.order - b.default.order);
+
+ form.setFieldValue('defaultSorters', JSON.stringify(defaultSorters));
+ }
+ // Set the state
+ setState(data.data);
+ }
+ // Something went wrong fetching filter data
+ else {
+ setState(null);
+ }
+ setIsLoading(false);
+ }, [templateId, form]);
+
+ useEffect(() => {
if (templateId) {
fetch();
+
}
- }, [templateId]);
+ }, [templateId, fetch]);
+ const filters = useMemo(() => state?.filters || [], [state]);
+ const sorters = useMemo(() => state?.sorters || [], [state]);
- // Conditional display of filters and sorters
if (!templateId) return null;
if (isLoading) return ;
if (!state) return null;
- // Filters and Sorters data available
return (
{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")}
-
-
-
- );
- }}
-
-
-
+ {filters.length > 0 && (
+
)}
- {state.sorters && state.sorters.length > 0 && (
-
-
- {(fields, {add, remove, move}) => {
- return (
-
- Sorters
- {fields.map((field, index) => (
-
-
-
-
- ({
- value: f.name,
- label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
- }))
- : []
- }
- />
-
-
-
-
-
-
-
-
-
- {
- remove(field.name);
- }}
- />
-
-
-
- ))}
-
- {
- add();
- }}
- style={{width: "100%"}}
- >
- {t("general.actions.add")}
-
-
-
- );
- }}
-
-
+ {sorters.length > 0 && (
+
)}
)}
diff --git a/client/src/components/report-center-modal/report-center-modal-utils.js b/client/src/components/report-center-modal/report-center-modal-utils.js
new file mode 100644
index 000000000..97b693a61
--- /dev/null
+++ b/client/src/components/report-center-modal/report-center-modal-utils.js
@@ -0,0 +1,137 @@
+import {uniqBy} from "lodash";
+
+/**
+ * Get value from path
+ * @param obj
+ * @param path
+ * @returns {*}
+ */
+const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
+
+/**
+ * Generate options from array
+ * @param bodyshop
+ * @param path
+ * @returns {unknown[]}
+ */
+const generateOptionsFromArray = (bodyshop, path) => {
+ const options = getValueFromPath(bodyshop, path);
+ return uniqBy(options.map((value) => ({
+ label: value,
+ value: value,
+ })), 'value');
+}
+
+
+/**
+ * Valid internal reflections
+ * Note: This is intended for future functionality
+ * @type {{special: string[], bodyshop: [{name: string, type: string}]}}
+ */
+const VALID_INTERNAL_REFLECTIONS = {
+ bodyshop: [
+ {
+ name: 'md_ro_statuses.statuses',
+ type: 'kv-to-v'
+ }
+ ],
+};
+
+/**
+ * Generate options
+ * @param bodyshop
+ * @param path
+ * @param labelPath
+ * @param valuePath
+ * @returns {{label: *, value: *}[]}
+ */
+const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
+ const options = getValueFromPath(bodyshop, path);
+ return uniqBy(Object.values(options).map((value) => ({
+ label: value[labelPath],
+ value: value[valuePath],
+ })), 'value');
+}
+
+/**
+ * Generate special reflections
+ * @param bodyshop
+ * @param finalPath
+ * @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
+ */
+const generateSpecialReflections = (bodyshop, finalPath) => {
+ switch (finalPath) {
+ // Special case because Referral Sources is an Array, not an Object.
+ case 'referral_source':
+ return generateOptionsFromArray(bodyshop, 'md_referral_sources');
+ case 'class':
+ return generateOptionsFromArray(bodyshop, 'md_classes');
+ case 'cost_centers':
+ return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
+ // Special case because Categories is an Array, not an Object.
+ case 'categories':
+ return generateOptionsFromArray(bodyshop, 'md_categories');
+ case 'insurance_companies':
+ return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
+ case 'employee_teams':
+ return generateOptionsFromObject(bodyshop, 'employee_teams', 'name', 'id');
+ // Special case because Employees uses a concatenation of first_name and last_name
+ case 'employees':
+ const employeesOptions = getValueFromPath(bodyshop, 'employees');
+ return uniqBy(Object.values(employeesOptions).map((value) => ({
+ label: `${value.first_name} ${value.last_name}`,
+ value: value.id,
+ })), 'value');
+ case 'last_names':
+ return generateOptionsFromObject(bodyshop, 'employees', 'last_name', 'last_name');
+ case 'first_names':
+ return generateOptionsFromObject(bodyshop, 'employees', 'first_name', 'first_name');
+ case 'job_statuses':
+ const statusOptions = getValueFromPath(bodyshop, 'md_ro_statuses.statuses');
+ return Object.values(statusOptions).map((value) => ({
+ label: value,
+ value
+ }));
+ default:
+ console.error('Invalid Special reflection provided by Report Filters');
+ return [];
+ }
+}
+
+/**
+ * Generate bodyshop reflections
+ * @param bodyshop
+ * @param finalPath
+ * @returns {{label: *, value: *}[]|*[]}
+ */
+const generateBodyshopReflections = (bodyshop, finalPath) => {
+ const options = getValueFromPath(bodyshop, finalPath);
+ const reflectionRenderer = VALID_INTERNAL_REFLECTIONS.bodyshop.find(reflection => reflection.name === finalPath);
+ if (reflectionRenderer?.type === 'kv-to-v') {
+ return Object.values(options).map((value) => ({
+ label: value,
+ value
+ }));
+ }
+ return [];
+}
+
+/**
+ * Generate internal reflections based on the path and bodyshop
+ * @param bodyshop
+ * @param upperPath
+ * @param finalPath
+ * @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
+ */
+const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
+ switch (upperPath) {
+ case 'special':
+ return generateSpecialReflections(bodyshop, finalPath);
+ case 'bodyshop':
+ return generateBodyshopReflections(bodyshop, finalPath);
+ default:
+ return [];
+ }
+};
+
+export {generateInternalReflections}
\ 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 c82a1e145..b9610335f 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
@@ -18,10 +18,11 @@ import EmployeeSearchSelect from "../employee-search-select/employee-search-sele
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import "./report-center-modal.styles.scss";
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
+import {selectBodyshop} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
- reportCenterModal: selectReportCenter,
- bodyshop: selectBodyshop,
+ reportCenterModal: selectReportCenter,
+ bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -89,22 +90,28 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
const end = values.dates ? values.dates[1] : null;
const { id } = values;
- await GenerateDocument(
- {
+ const templateConfig = {
name: values.key,
variables: {
- ...(start
- ? { start: moment(start).startOf("day").format("YYYY-MM-DD") }
- : {}),
- ...(end ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } : {}),
- ...(start ? { starttz: moment(start).startOf("day") } : {}),
- ...(end ? { endtz: moment(end).endOf("day") } : {}),
+ ...(start
+ ? {start: moment(start).startOf("day").format("YYYY-MM-DD")}
+ : {}),
+ ...(end ? {end: moment(end).endOf("day").format("YYYY-MM-DD")} : {}),
+ ...(start ? {starttz: moment(start).startOf("day")} : {}),
+ ...(end ? {endtz: moment(end).endOf("day")} : {}),
- ...(id ? { id: id } : {}),
+ ...(id ? {id: id} : {}),
},
filters: values.filters,
sorters: values.sorters,
- },
+ };
+
+ if (_.isString(values.defaultSorters) && !_.isEmpty(values.defaultSorters)) {
+ templateConfig.defaultSorters = JSON.parse(values.defaultSorters);
+ }
+
+ await GenerateDocument(
+ templateConfig,
{
to: values.to,
subject: Templates[values.key]?.subject,
@@ -142,7 +149,8 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
onChange={(e) => setSearch(e.target.value)}
value={search}
/>
-
+
-
+
{() => {
const key = form.getFieldValue("key");
@@ -261,6 +269,9 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
{() => {
const key = form.getFieldValue("key");
const datedisable = Templates[key] && Templates[key].datedisable;
+
+ // TODO: MERGE NOTE, Ranges turns to presets in DatePicker.RangePicker
+
if (datedisable !== true) {
return (
);
diff --git a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx
index cfd6d945d..ecaf189fb 100644
--- a/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx
+++ b/client/src/components/scoreboard-day-stats/scoreboard-day-stats.component.jsx
@@ -25,6 +25,8 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
return acc + value.bodyhrs;
}, 0);
+ const numJobs = entries.length;
+
return (
bodyHrs ? "red" : "green" }}
- label="B"
+ label="Body"
value={bodyHrs.toFixed(1)}
/>
paintHrs ? "red" : "green" }}
- label="P"
+ label="Refinish"
value={paintHrs.toFixed(1)}
/>
-
-
+
+
+
);
}
diff --git a/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx b/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx
index a8488e9dd..31141b628 100644
--- a/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx
+++ b/client/src/components/scoreboard-entry-edit/scoreboard-entry-edit.component.jsx
@@ -1,5 +1,14 @@
import { useMutation } from "@apollo/client";
-import { Button, Card, Dropdown, Form, InputNumber, notification } from "antd";
+import {
+ Button,
+ Card,
+ Dropdown,
+ Form,
+ InputNumber,
+ notification,
+ Space,
+} from "antd";
+import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
@@ -13,6 +22,7 @@ export default function ScoreboardEntryEdit({ entry }) {
const handleFinish = async (values) => {
setLoading(true);
+ values.date = moment(values.date).format("YYYY-MM-DD");
const result = await updateScoreboardentry({
variables: { sbId: entry.id, sbInput: values },
});
@@ -77,13 +87,14 @@ export default function ScoreboardEntryEdit({ entry }) {
>
-
-
- {t("general.actions.save")}
-
- setVisible(false)}>
- {t("general.actions.cancel")}
-
+
+
+ {t("general.actions.save")}
+
+ setVisible(false)}>
+ {t("general.actions.cancel")}
+
+
);
diff --git a/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx b/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx
index 6b1c13ca3..67267fb3f 100644
--- a/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx
+++ b/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx
@@ -1,3 +1,4 @@
+import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Input, Modal, Space, Table, Typography } from "antd";
import React, { useState } from "react";
@@ -5,12 +6,14 @@ import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries";
import { DateFormatter } from "../../utils/DateFormatter";
+import { pageLimit } from "../../utils/config";
+import { alphaSort, dateSort } from "../../utils/sorters";
import AlertComponent from "../alert/alert.component";
-import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
+import OwnerNameDisplay, {
+ OwnerNameDisplayFunction,
+} from "../owner-name-display/owner-name-display.component";
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
-import { SyncOutlined } from "@ant-design/icons";
-import {pageLimit} from "../../utils/config";
export default function ScoreboardJobsList({ scoreBoardlist }) {
const { t } = useTranslation();
const [state, setState] = useState({
@@ -44,6 +47,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
+ sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
render: (text, record) => (
{record.job.ro_number || t("general.labels.na")}
@@ -55,7 +59,11 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
dataIndex: "owner",
key: "owner",
ellipsis: true,
-
+ sorter: (a, b) =>
+ alphaSort(
+ OwnerNameDisplayFunction(a.job),
+ OwnerNameDisplayFunction(b.job)
+ ),
render: (text, record) => ,
},
{
@@ -63,6 +71,15 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
+ sorter: (a, b) =>
+ alphaSort(
+ `${a.job.v_model_yr || ""} ${a.job.v_make_desc || ""} ${
+ a.job.v_model_desc || ""
+ }`,
+ `${b.job.v_model_yr || ""} ${b.job.v_make_desc || ""} ${
+ b.job.v_model_desc || ""
+ }`
+ ),
render: (text, record) => (
{`${record.job.v_model_yr || ""} ${
record.job.v_make_desc || ""
@@ -73,17 +90,20 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
title: t("scoreboard.fields.date"),
dataIndex: "date",
key: "date",
+ sorter: (a, b) => dateSort(a.date, b.date),
render: (text, record) => {record.date} ,
},
- {
- title: t("scoreboard.fields.painthrs"),
- dataIndex: "painthrs",
- key: "painthrs",
- },
{
title: t("scoreboard.fields.bodyhrs"),
dataIndex: "bodyhrs",
key: "bodyhrs",
+ sorter: (a, b) => Number(a.bodyhrs) - Number(b.bodyhrs),
+ },
+ {
+ title: t("scoreboard.fields.painthrs"),
+ dataIndex: "painthrs",
+ key: "painthrs",
+ sorter: (a, b) => Number(a.painthrs) - Number(b.painthrs),
},
{
title: t("general.labels.actions"),
@@ -104,8 +124,9 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
visible={state.visible}
destroyOnClose
width="80%"
+ closable={false}
cancelButtonProps={{ style: { display: "none" } }}
- onCancel={() =>
+ onOk={() =>
setState((state) => ({
...state,
visible: false,
diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
index 112d441f6..11129b025 100644
--- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
+++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
@@ -29,10 +29,13 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
let ret = {
todayBody: 0,
todayPaint: 0,
+ todayJobs: 0,
weeklyPaint: 0,
+ weeklyJobs: 0,
weeklyBody: 0,
toDateBody: 0,
toDatePaint: 0,
+ toDateJobs: 0,
};
const today = moment();
@@ -40,6 +43,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
ret.todayBody = ret.todayBody + d.bodyhrs;
ret.todayPaint = ret.todayPaint + d.painthrs;
+ ret.todayJobs++;
});
}
@@ -49,6 +53,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
+ ret.weeklyJobs++;
});
}
StartOfWeek = StartOfWeek.add(1, "day");
@@ -60,6 +65,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
ret.toDateBody = ret.toDateBody + d.bodyhrs;
ret.toDatePaint = ret.toDatePaint + d.painthrs;
+ ret.toDateJobs++;
});
}
startOfMonth = startOfMonth.add(1, "day");
@@ -87,7 +93,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
@@ -140,7 +146,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
@@ -181,7 +187,12 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/graphql/courtesy-car.queries.js b/client/src/graphql/courtesy-car.queries.js
index 4f7bcd0ca..eef9a759f 100644
--- a/client/src/graphql/courtesy-car.queries.js
+++ b/client/src/graphql/courtesy-car.queries.js
@@ -22,6 +22,7 @@ export const QUERY_AVAILABLE_CC = gql`
]
status: { _eq: "courtesycars.status.in" }
}
+ order_by: { fleetnumber: asc }
) {
color
dailycost
@@ -29,6 +30,7 @@ export const QUERY_AVAILABLE_CC = gql`
fleetnumber
fuel
id
+ insuranceexpires
make
mileage
model
@@ -57,7 +59,7 @@ export const CHECK_CC_FLEET_NUMBER = gql`
`;
export const QUERY_ALL_CC = gql`
query QUERY_ALL_CC {
- courtesycars {
+ courtesycars(order_by: { fleetnumber: asc }) {
color
created_at
dailycost
diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx
index 3a700a89e..9ed0133a9 100644
--- a/client/src/pages/dms/dms.container.jsx
+++ b/client/src/pages/dms/dms.container.jsx
@@ -26,10 +26,12 @@ import { OwnerNameDisplayFunction } from "../../components/owner-name-display/ow
import { auth } from "../../firebase/firebase.utils";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import {
+ insertAuditTrail,
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
+import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -38,15 +40,17 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
+ insertAuditTrail: ({ jobid, operation }) =>
+ dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export const socket = SocketIO(
- // process.env.NODE_ENV === "production"
- // ? process.env.REACT_APP_AXIOS_BASE_API_URL
- // : window.location.origin,
- "http://localhost:4000", // for dev testing,
+ process.env.NODE_ENV === "production"
+ ? process.env.REACT_APP_AXIOS_BASE_API_URL
+ : window.location.origin,
+ // "http://localhost:4000", // for dev testing,
{
path: "/ws",
withCredentials: true,
@@ -57,7 +61,12 @@ export const socket = SocketIO(
}
);
-export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
+export function DmsContainer({
+ bodyshop,
+ setBreadcrumbs,
+ setSelectedHeader,
+ insertAuditTrail,
+}) {
const { t } = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useHistory();
@@ -115,6 +124,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
notification.success({
message: t("jobs.successes.exported"),
});
+ insertAuditTrail({
+ jobid: payload,
+ operation: AuditTrailMapping.jobexported(),
+ });
history.push("/manage/accounting/receivables");
});
diff --git a/client/src/redux/application/application.sagas.js b/client/src/redux/application/application.sagas.js
index 447fed0e6..fed628415 100644
--- a/client/src/redux/application/application.sagas.js
+++ b/client/src/redux/application/application.sagas.js
@@ -289,7 +289,7 @@ export function* insertAuditTrailSaga({
fields: {
audit_trail(existingAuditTrail, { readField }) {
const newAuditTrail = cache.writeQuery({
- data: data.insert_audit_trail_one,
+ data: data,
query: INSERT_AUDIT_TRAIL,
variables,
});
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 9b6fbac55..c62de199a 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -109,6 +109,7 @@
"appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.",
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
"assignedlinehours": "Assigned job lines totaling {{hours}} units to {{team}}.",
+ "billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
"billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
"failedpayment": "Failed payment attempt.",
@@ -116,6 +117,7 @@
"jobassignmentremoved": "Employee assignment removed for {{operation}}",
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
"jobconverted": "Job converted and assigned number {{ro_number}}.",
+ "jobexported": "Job has been exported.",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}",
@@ -128,7 +130,9 @@
"jobspartsorder": "Parts order {{order_number}} added to Job.",
"jobspartsreturn": "Parts return {{order_number}} added to Job.",
"jobstatuschange": "Job status changed to {{status}}.",
- "jobsupplement": "Job supplement imported."
+ "jobsupplement": "Job supplement imported.",
+ "jobsuspend": "Suspend Toggle set to {{status}}",
+ "jobvoid": "Job has been voided."
}
},
"billlines": {
@@ -760,6 +764,7 @@
"driverinformation": "Driver's Information",
"findcontract": "Find Contract",
"findermodal": "Contract Finder",
+ "insuranceexpired": "The courtesy car insurance expires before the car is expected to return.",
"noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.",
"populatefromjob": "Populate from Job",
"rates": "Contract Rates",
@@ -887,6 +892,7 @@
"labels": {
"bodyhrs": "Body Hrs",
"dollarsinproduction": "Dollars in Production",
+ "phone": "Phone",
"prodhrs": "Production Hrs",
"refhrs": "Refinish Hrs"
},
@@ -902,8 +908,10 @@
"productiondollars": "Total Dollars in Production",
"productionhours": "Total Hours in Production",
"projectedmonthlysales": "Projected Monthly Sales",
- "scheduledintoday": "Sheduled In Today: {{date}}",
- "scheduledouttoday": "Sheduled Out Today: {{date}}"
+ "scheduledindate": "Sheduled In Today: {{date}}",
+ "scheduledintoday": "Sheduled In Today",
+ "scheduledoutdate": "Sheduled Out Today: {{date}}",
+ "scheduledouttoday": "Sheduled Out Today"
}
},
"dms": {
@@ -2674,6 +2682,13 @@
"advanced_filters_hide": "Hide",
"advanced_filters_filters": "Filters",
"advanced_filters_sorters": "Sorters",
+ "advanced_filters_filter_field": "Field",
+ "advanced_filters_sorter_field": "Field",
+ "advanced_filters_true": "True",
+ "advanced_filters_false": "False",
+ "advanced_filters_sorter_direction": "Direction",
+ "advanced_filters_filter_operator": "Operator",
+ "advanced_filters_filter_value": "Value",
"dates": "Dates",
"employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}",
@@ -2849,6 +2864,7 @@
"allemployeetimetickets": "All Employee Time Tickets",
"asoftodaytarget": "As of Today",
"body": "Body",
+ "bodyabbrev": "B",
"bodycharttitle": "Body Targets vs Actual",
"calendarperiod": "Periods based on calendar weeks/months.",
"combinedcharttitle": "Combined Targets vs Actual",
@@ -2865,6 +2881,7 @@
"productivestatistics": "Productive Hours Statistics",
"productivetimeticketsoverdate": "Productive Hours over Selected Dates",
"refinish": "Refinish",
+ "refinishabbrev": "R",
"refinishcharttitle": "Refinish Targets vs Actual",
"targets": "Targets",
"thismonth": "This Month",
@@ -2872,6 +2889,7 @@
"timetickets": "Time Tickets",
"timeticketsemployee": "Time Tickets by Employee",
"todateactual": "Actual (MTD)",
+ "total": "Total",
"totalhrs": "Total Hours",
"totaloverperiod": "Total over Selected Dates",
"weeklyactual": "Actual (W)",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index deb74ac7c..3b688bfed 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -109,6 +109,7 @@
"appointmentcancel": "",
"appointmentinsert": "",
"assignedlinehours": "",
+ "billdeleted": "",
"billposted": "",
"billupdated": "",
"failedpayment": "",
@@ -116,6 +117,7 @@
"jobassignmentremoved": "",
"jobchecklist": "",
"jobconverted": "",
+ "jobexported": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
@@ -128,7 +130,9 @@
"jobspartsorder": "",
"jobspartsreturn": "",
"jobstatuschange": "",
- "jobsupplement": ""
+ "jobsupplement": "",
+ "jobsuspend": "",
+ "jobvoid": ""
}
},
"billlines": {
@@ -760,6 +764,7 @@
"driverinformation": "",
"findcontract": "",
"findermodal": "",
+ "insuranceexpired": "",
"noteconvertedfrom": "",
"populatefromjob": "",
"rates": "",
@@ -887,6 +892,7 @@
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
+ "phone": "",
"prodhrs": "",
"refhrs": ""
},
@@ -902,7 +908,9 @@
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": "",
+ "scheduledindate": "",
"scheduledintoday": "",
+ "scheduledoutdate": "",
"scheduledouttoday": ""
}
},
@@ -2674,6 +2682,13 @@
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
+ "advanced_filters_filter_field": "",
+ "advanced_filters_sorter_field": "",
+ "advanced_filters_true": "",
+ "advanced_filters_false": "",
+ "advanced_filters_sorter_direction": "",
+ "advanced_filters_filter_operator": "",
+ "advanced_filters_filter_value": "",
"dates": "",
"employee": "",
"filterson": "",
@@ -2849,6 +2864,7 @@
"allemployeetimetickets": "",
"asoftodaytarget": "",
"body": "",
+ "bodyabbrev": "",
"bodycharttitle": "",
"calendarperiod": "",
"combinedcharttitle": "",
@@ -2865,6 +2881,7 @@
"productivestatistics": "",
"productivetimeticketsoverdate": "",
"refinish": "",
+ "refinishabbrev": "",
"refinishcharttitle": "",
"targets": "",
"thismonth": "",
@@ -2872,6 +2889,7 @@
"timetickets": "",
"timeticketsemployee": "",
"todateactual": "",
+ "total": "",
"totalhrs": "",
"totaloverperiod": "",
"weeklyactual": "",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 0ececca68..8f6540cb0 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -109,6 +109,7 @@
"appointmentcancel": "",
"appointmentinsert": "",
"assignedlinehours": "",
+ "billdeleted": "",
"billposted": "",
"billupdated": "",
"failedpayment": "",
@@ -116,6 +117,7 @@
"jobassignmentremoved": "",
"jobchecklist": "",
"jobconverted": "",
+ "jobexported": "",
"jobfieldchanged": "",
"jobimported": "",
"jobinproductionchange": "",
@@ -128,7 +130,9 @@
"jobspartsorder": "",
"jobspartsreturn": "",
"jobstatuschange": "",
- "jobsupplement": ""
+ "jobsupplement": "",
+ "jobsuspend": "",
+ "jobvoid": ""
}
},
"billlines": {
@@ -760,6 +764,7 @@
"driverinformation": "",
"findcontract": "",
"findermodal": "",
+ "insuranceexpired": "",
"noteconvertedfrom": "",
"populatefromjob": "",
"rates": "",
@@ -887,6 +892,7 @@
"labels": {
"bodyhrs": "",
"dollarsinproduction": "",
+ "phone": "",
"prodhrs": "",
"refhrs": ""
},
@@ -902,7 +908,9 @@
"productiondollars": "",
"productionhours": "",
"projectedmonthlysales": "",
+ "scheduledindate": "",
"scheduledintoday": "",
+ "scheduledoutdate": "",
"scheduledouttoday": ""
}
},
@@ -2674,6 +2682,13 @@
"advanced_filters_hide": "",
"advanced_filters_filters": "",
"advanced_filters_sorters": "",
+ "advanced_filters_filter_field": "",
+ "advanced_filters_sorter_field": "",
+ "advanced_filters_true": "",
+ "advanced_filters_false": "",
+ "advanced_filters_sorter_direction": "",
+ "advanced_filters_filter_operator": "",
+ "advanced_filters_filter_value": "",
"dates": "",
"employee": "",
"filterson": "",
@@ -2849,6 +2864,7 @@
"allemployeetimetickets": "",
"asoftodaytarget": "",
"body": "",
+ "bodyabbrev": "",
"bodycharttitle": "",
"calendarperiod": "",
"combinedcharttitle": "",
@@ -2865,6 +2881,7 @@
"productivestatistics": "",
"productivetimeticketsoverdate": "",
"refinish": "",
+ "refinishabbrev": "",
"refinishcharttitle": "",
"targets": "",
"thismonth": "",
@@ -2872,6 +2889,7 @@
"timetickets": "",
"timeticketsemployee": "",
"todateactual": "",
+ "total": "",
"totalhrs": "",
"totaloverperiod": "",
"weeklyactual": "",
diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js
index 3afe3ec93..5ad69c04f 100644
--- a/client/src/utils/AuditTrailMappings.js
+++ b/client/src/utils/AuditTrailMappings.js
@@ -14,6 +14,8 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
appointmentinsert: (start) =>
i18n.t("audit_trail.messages.appointmentinsert", { start }),
+ billdeleted: (invoice_number) =>
+ i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
billposted: (invoice_number) =>
i18n.t("audit_trail.messages.billposted", { invoice_number }),
billupdated: (invoice_number) =>
@@ -26,6 +28,7 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
jobconverted: (ro_number) =>
i18n.t("audit_trail.messages.jobconverted", { ro_number }),
+ jobexported: () => i18n.t("audit_trail.messages.jobexported"),
jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
@@ -53,6 +56,8 @@ const AuditTrailMapping = {
jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", { status }),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
+ jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }),
+ jobvoid: () => i18n.t("audit_trail.messages.jobvoid"),
};
export default AuditTrailMapping;
diff --git a/client/src/utils/DatePickerRanges.js b/client/src/utils/DatePickerRanges.js
index aeed206c5..95ee80a45 100644
--- a/client/src/utils/DatePickerRanges.js
+++ b/client/src/utils/DatePickerRanges.js
@@ -24,4 +24,13 @@ const range = {
],
"Last 90 Days": [moment().add(-90, "days"), moment()],
};
+
+// We are development, lets get crazy
+if (process.env.NODE_ENV === "development") {
+ range["Last year"] = [
+ moment().subtract(1, "year"),
+ moment(),
+ ];
+}
+
export default range;
diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js
index 33121f6b0..000667dbe 100644
--- a/client/src/utils/RenderTemplate.js
+++ b/client/src/utils/RenderTemplate.js
@@ -9,7 +9,7 @@ import {store} from "../redux/store";
import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios";
import {TemplateList} from "./TemplateConstants";
-import {applyFilters, applySorters, parseQuery, printQuery, wrapFiltersInAnd} from "./graphQLmodifier";
+import {generateTemplate} from "./graphQLmodifier";
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
@@ -75,7 +75,10 @@ export default async function RenderTemplate(
headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop,
+ filters: templateObject?.filters,
+ sorters: templateObject?.sorters,
offset: bodyshop.timezone, //dayjs().utcOffset(),
+ defaultSorters: templateObject?.defaultSorters,
},
};
@@ -278,7 +281,9 @@ export const GenerateDocument = async (
sendType,
jobid
) => {
+
const bodyshop = store.getState().user.bodyshop;
+
if (sendType === "e") {
store.dispatch(
setEmailOptions({
@@ -402,9 +407,12 @@ const fetchContextData = async (templateObject, jsrAuth) => {
// console.log('Unmodified Query');
// console.dir(templateQueryToExecute);
+ const hasFilters = templateObject?.filters?.length > 0;
+ const hasSorters = templateObject?.sorters?.length > 0;
+ const hasDefaultSorters = templateObject?.defaultSorters?.length > 0;
// We have no template filters or sorters, so we can just execute the query and return the data
- if ((!templateObject?.filters && !templateObject?.filters?.length && !templateObject?.sorters && !templateObject?.sorters?.length)) {
+ if (!hasFilters && !hasSorters && !hasDefaultSorters) {
let contextData = {};
if (templateQueryToExecute) {
const {data} = await client.query({
@@ -417,36 +425,11 @@ const fetchContextData = async (templateObject, jsrAuth) => {
return {contextData, useShopSpecificTemplate};
}
- // Parse the query and apply the filters and sorters
- const ast = parseQuery(templateQueryToExecute);
-
- let filterFields = [];
-
- if (templateObject?.filters && templateObject?.filters?.length) {
- applyFilters(ast, templateObject.filters, filterFields);
- wrapFiltersInAnd(ast, filterFields);
- }
-
- if (templateObject?.sorters && templateObject?.sorters?.length) {
- applySorters(ast, templateObject.sorters);
- }
-
- const finalQuery = printQuery(ast);
-
- // commented out for future revision debugging
- // console.log('Modified Query');
- // console.log(finalQuery);
-
- let contextData = {};
- if (templateQueryToExecute) {
- const {data} = await client.query({
- query: gql(finalQuery),
- variables: {...templateObject.variables},
- });
- contextData = data;
- }
-
- return {contextData, useShopSpecificTemplate};
+ return await generateTemplate(
+ templateQueryToExecute,
+ templateObject,
+ useShopSpecificTemplate
+ );
};
//export const displayTemplateInWindow = (html) => {
diff --git a/client/src/utils/graphQLmodifier.js b/client/src/utils/graphQLmodifier.js
index 5716753ad..d31c6584a 100644
--- a/client/src/utils/graphQLmodifier.js
+++ b/client/src/utils/graphQLmodifier.js
@@ -1,32 +1,96 @@
import {Kind, parse, print, visit} from "graphql";
+import client from "./GraphQLClient";
+import {gql} from "@apollo/client";
+/* eslint-disable no-loop-func */
+
+/**
+ * The available operators for filtering (string)
+ * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
+ */
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"}
+ {value: "_nilike", label: "does not contain case-insensitive"},
+ {value: "_in", label: "in", type: "array"},
+ {value: "_nin", label: "not in", type: "array"}
];
+
+/**
+ * The available operators for filtering (dates)
+ * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
+ */
+const DATE_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"},
+ {value: "_in", label: "in", type: "array"},
+ {value: "_nin", label: "not in", type: "array"}
+];
+
+/**
+ * The available operators for filtering (booleans)
+ * @type {[{label: string, value: string},{label: string, value: string}]}
+ */
+const BOOLEAN_OPERATORS = [
+ {value: "_eq", label: "equals"},
+ {value: "_neq", label: "does not equal"},
+];
+
+/**
+ * The available operators for filtering (numbers)
+ * @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
+ */
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"}
+ {value: "_lte", label: "less than or equal"},
+ {value: "_in", label: "in", type: "array"},
+ {value: "_nin", label: "not in", type: "array"}
];
-export function getOperatorsByType(type = 'string') {
+/**
+ * The available operators for sorting
+ * @type {[{label: string, value: string},{label: string, value: string}]}
+ */
+const ORDER_BY_OPERATORS = [
+ {value: "asc", label: "ascending"},
+ {value: "desc", label: "descending"}
+];
+
+/**
+ * Get the available operators for filtering
+ * @returns {[{label: string, value: string},{label: string, value: string}]}
+ */
+export function getOrderOperatorsByType() {
+ return ORDER_BY_OPERATORS;
+}
+
+/**
+ * Get the available operators for filtering
+ * @param type
+ * @returns {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null]}
+ */
+export function getWhereOperatorsByType(type = 'string') {
const operators = {
string: STRING_OPERATORS,
- number: NUMBER_OPERATORS
+ number: NUMBER_OPERATORS,
+ boolean: BOOLEAN_OPERATORS,
+ bool: BOOLEAN_OPERATORS,
+ date: DATE_OPERATORS
};
return operators[type];
}
-/* eslint-disable no-loop-func */
-
/**
* Parse a GraphQL query into an AST
* @param query
@@ -44,6 +108,49 @@ export function parseQuery(query) {
export function printQuery(query) {
return print(query);
}
+
+/**
+ * Generate a template based on the query and object
+ * @param templateQueryToExecute
+ * @param templateObject
+ * @param useShopSpecificTemplate
+ * @returns {Promise<{contextData: {}, useShopSpecificTemplate}>}
+ */
+export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) {
+ // Advanced Filtering and Sorting modifications start here
+
+ // Parse the query and apply the filters and sorters
+ const ast = parseQuery(templateQueryToExecute);
+
+
+ if (templateObject?.filters && templateObject?.filters?.length) {
+ applyFilters(ast, templateObject.filters);
+ }
+
+ if (templateObject?.sorters && templateObject?.sorters?.length) {
+ applySorters(ast, templateObject.sorters);
+ } else if (templateObject?.defaultSorters && templateObject?.defaultSorters?.length) {
+ applySorters(ast, templateObject.defaultSorters);
+ }
+
+ const finalQuery = printQuery(ast);
+
+ // commented out for future revision debugging
+ // console.log('Modified Query');
+ // console.log(finalQuery);
+
+ let contextData = {};
+ if (templateQueryToExecute) {
+ const {data} = await client.query({
+ query: gql(finalQuery),
+ variables: {...templateObject.variables},
+ });
+ contextData = data;
+ }
+
+ return {contextData, useShopSpecificTemplate};
+}
+
/**
* Apply sorters to the AST
* @param ast
@@ -83,16 +190,16 @@ export function applySorters(ast, sorters) {
if (!orderByArg) {
orderByArg = {
kind: Kind.ARGUMENT,
- name: { kind: Kind.NAME, value: 'order_by' },
- value: { kind: Kind.OBJECT, fields: [] },
+ 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
+ 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
@@ -104,10 +211,59 @@ export function applySorters(ast, sorters) {
});
}
+/**
+ * Apply Top Level Sub to the AST
+ * @param node
+ * @param fieldPath
+ * @param filterField
+ */
+function applyTopLevelSub(node, fieldPath, filterField) {
+ // Find or create the where argument for the top-level subfield
+ 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 topLevelSubSelection = node.selectionSet.selections.find(selection =>
+ selection.name.value === fieldPath[0]
+ );
+ if (topLevelSubSelection) {
+ topLevelSubSelection.arguments = topLevelSubSelection.arguments || [];
+ topLevelSubSelection.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(filterField);
+ } else { // Directly under the top level
+ whereArg.value.fields.push(filterField);
+ }
+}
+
/**
* Apply filters to the AST
* @param ast
* @param filters
+ * @returns {ASTNode}
*/
export function applyFilters(ast, filters) {
return visit(ast, {
@@ -116,152 +272,179 @@ export function applyFilters(ast, filters) {
filters.forEach(filter => {
const fieldPath = filter.field.split('.');
let topLevel = false;
+ let topLevelSub = 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
+ if (fieldPath.length === 2) {
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);
- }
+ if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
+ fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
+ topLevelSub = true;
}
+ // Construct the filter for a top-level application
+ const targetFieldName = fieldPath[fieldPath.length - 1];
+
+ let filterValue = createFilterValue(filter);
+ let filterField = createFilterField(targetFieldName, filter, filterValue);
+
+ if (topLevel) {
+ applyTopLevelFilter(node, fieldPath, filterField);
+ } else if (topLevelSub) {
+ applyTopLevelSub(node, fieldPath, filterField);
+ } else {
+ applyNestedFilter(node, fieldPath, filterField);
+ }
});
}
}
});
}
+/**
+ * Create a filter value based on the filter
+ * @param filter
+ * @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
+ */
+function createFilterValue(filter) {
+ if (Array.isArray(filter.value)) {
+ // If it's an array, create a list value with the array items
+ return {
+ kind: Kind.LIST,
+ values: filter.value.map(item => ({
+ kind: getGraphQLKind(item),
+ value: item,
+ })),
+ };
+ } else {
+ // If it's not an array, use the existing logic
+ return {
+ kind: getGraphQLKind(filter.value),
+ value: filter.value,
+ };
+ }
+}
+/**
+ * Create a filter field based on the target field and filter
+ * @param targetFieldName
+ * @param filter
+ * @param filterValue
+ * @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}}
+ */
+function createFilterField(targetFieldName, filter, filterValue) {
+ return {
+ 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,
+ }],
+ },
+ };
+}
+
+/**
+ * Apply a top-level filter to the AST
+ * @param node
+ * @param fieldPath
+ * @param filterField
+ */
+function applyTopLevelFilter(node, fieldPath, filterField) {
+ // 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(filterField);
+ } else { // Directly under the top level
+ whereArg.value.fields.push(filterField);
+ }
+}
+
+/**
+ * Apply a nested filter to the AST
+ * @param node
+ * @param fieldPath
+ * @param filterField
+ */
+function applyNestedFilter(node, fieldPath, filterField) {
+ // Initialize a reference to the current selection to traverse down the AST
+ let currentSelection = node;
+
+ // 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) {
+ // 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
+ });
+ }
+
+ // Add the filter field to the 'where' clause of the current selection
+ currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
+}
/**
* Get the GraphQL kind for a value
@@ -269,41 +452,17 @@ export function applyFilters(ast, filters) {
* @returns {Kind|Kind.INT}
*/
function getGraphQLKind(value) {
- if (typeof value === 'number') {
+ if (Array.isArray(value)) {
+ return Kind.LIST;
+ } else 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;
+ } else if (value instanceof Date) {
+ return Kind.STRING; // GraphQL does not have a Date type, so we return it as a 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
+/* eslint-enable no-loop-func */
diff --git a/package-lock.json b/package-lock.json
index 739376162..c105c511f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@aws-sdk/client-secrets-manager": "^3.454.0",
"@aws-sdk/client-ses": "^3.454.0",
"@aws-sdk/credential-provider-node": "^3.451.0",
+ "@azure/storage-blob": "^12.17.0",
"@opensearch-project/opensearch": "^2.4.0",
"aws4": "^1.12.0",
"axios": "^1.6.2",
@@ -53,6 +54,7 @@
"xmlbuilder2": "^3.1.1"
},
"devDependencies": {
+ "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2",
"source-map-explorer": "^2.5.2"
},
@@ -696,11 +698,496 @@
"tslib": "^2.3.1"
}
},
- "node_modules/@babel/parser": {
+ "node_modules/@azure/abort-controller": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz",
+ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==",
+ "dependencies": {
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@azure/core-auth": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.6.0.tgz",
+ "integrity": "sha512-3X9wzaaGgRaBCwhLQZDtFp5uLIXCPrGbwJNWPPugvL4xbIGgScv77YzzxToKGLAKvG9amDoofMoP+9hsH1vs1w==",
+ "dependencies": {
+ "@azure/abort-controller": "^2.0.0",
+ "@azure/core-util": "^1.1.0",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
+ "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==",
+ "dependencies": {
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-http": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz",
+ "integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==",
+ "dependencies": {
+ "@azure/abort-controller": "^1.0.0",
+ "@azure/core-auth": "^1.3.0",
+ "@azure/core-tracing": "1.0.0-preview.13",
+ "@azure/core-util": "^1.1.1",
+ "@azure/logger": "^1.0.0",
+ "@types/node-fetch": "^2.5.0",
+ "@types/tunnel": "^0.0.3",
+ "form-data": "^4.0.0",
+ "node-fetch": "^2.6.7",
+ "process": "^0.11.10",
+ "tslib": "^2.2.0",
+ "tunnel": "^0.0.6",
+ "uuid": "^8.3.0",
+ "xml2js": "^0.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@azure/core-http/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/@azure/core-http/node_modules/xml2js": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
+ "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/@azure/core-http/node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/@azure/core-lro": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.6.0.tgz",
+ "integrity": "sha512-PyRNcaIOfMgoUC01/24NoG+k8O81VrKxYARnDlo+Q2xji0/0/j2nIt8BwQh294pb1c5QnXTDPbNR4KzoDKXEoQ==",
+ "dependencies": {
+ "@azure/abort-controller": "^2.0.0",
+ "@azure/core-util": "^1.2.0",
+ "@azure/logger": "^1.0.0",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
+ "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==",
+ "dependencies": {
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-paging": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz",
+ "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==",
+ "dependencies": {
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@azure/core-tracing": {
+ "version": "1.0.0-preview.13",
+ "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz",
+ "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==",
+ "dependencies": {
+ "@opentelemetry/api": "^1.0.1",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@azure/core-util": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.7.0.tgz",
+ "integrity": "sha512-Zq2i3QO6k9DA8vnm29mYM4G8IE9u1mhF1GUabVEqPNX8Lj833gdxQ2NAFxt2BZsfAL+e9cT8SyVN7dFVJ/Hf0g==",
+ "dependencies": {
+ "@azure/abort-controller": "^2.0.0",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/core-util/node_modules/@azure/abort-controller": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz",
+ "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==",
+ "dependencies": {
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@azure/logger": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz",
+ "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==",
+ "dependencies": {
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@azure/storage-blob": {
+ "version": "12.17.0",
+ "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz",
+ "integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==",
+ "dependencies": {
+ "@azure/abort-controller": "^1.0.0",
+ "@azure/core-http": "^3.0.0",
+ "@azure/core-lro": "^2.2.0",
+ "@azure/core-paging": "^1.1.1",
+ "@azure/core-tracing": "1.0.0-preview.13",
+ "@azure/logger": "^1.0.0",
+ "events": "^3.0.0",
+ "tslib": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
+ "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.23.4",
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.17.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
+ "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.17.0",
+ "jsesc": "^2.5.1",
+ "source-map": "^0.5.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/generator/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@babel/helper-environment-visitor": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name/node_modules/@babel/types": {
+ "version": "7.23.9",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
+ "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.23.4",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-hoist-variables": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": {
+ "version": "7.23.9",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
+ "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.23.4",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": {
+ "version": "7.23.9",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
+ "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.23.4",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
"version": "7.23.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz",
- "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==",
- "optional": true,
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
+ "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.23.9",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
+ "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
+ "devOptional": true,
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -719,6 +1206,97 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/template": {
+ "version": "7.23.9",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
+ "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.23.5",
+ "@babel/parser": "^7.23.9",
+ "@babel/types": "^7.23.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template/node_modules/@babel/types": {
+ "version": "7.23.9",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
+ "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.23.4",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/generator": {
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
+ "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.23.6",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "jsesc": "^2.5.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/types": {
+ "version": "7.23.9",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
+ "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.23.4",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
+ "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.16.7",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
@@ -1035,6 +1613,54 @@
"resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz",
"integrity": "sha512-bIcKnH5YmtTYr7S6J3J86dn/rFiklwRpOqbTOQ9C0WMmR9FKHVb3bxs2UYfqEmNb93O4nbA97sb6rtz33i9SyA=="
},
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz",
+ "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.23",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz",
+ "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
"node_modules/@jsdoc/salty": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz",
@@ -1107,6 +1733,14 @@
"yarn": "^1.22.10"
}
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz",
+ "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1715,6 +2349,29 @@
"node": ">= 6"
}
},
+ "node_modules/@trivago/prettier-plugin-sort-imports": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz",
+ "integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/generator": "7.17.7",
+ "@babel/parser": "^7.20.5",
+ "@babel/traverse": "7.23.2",
+ "@babel/types": "7.17.0",
+ "javascript-natural-sort": "0.7.1",
+ "lodash": "^4.17.21"
+ },
+ "peerDependencies": {
+ "@vue/compiler-sfc": "3.x",
+ "prettier": "2.x - 3.x"
+ },
+ "peerDependenciesMeta": {
+ "@vue/compiler-sfc": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -1858,6 +2515,15 @@
"undici-types": "~5.26.4"
}
},
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.11",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
+ "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.0"
+ }
+ },
"node_modules/@types/qs": {
"version": "6.9.10",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
@@ -1907,6 +2573,14 @@
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
},
+ "node_modules/@types/tunnel": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz",
+ "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -3308,6 +3982,14 @@
"node": ">=6"
}
},
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -3855,6 +4537,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/google-auth-library": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz",
@@ -4447,6 +5138,12 @@
"node": "*"
}
},
+ "node_modules/javascript-natural-sort": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
+ "dev": true
+ },
"node_modules/jose": {
"version": "4.15.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz",
@@ -4460,6 +5157,12 @@
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="
},
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
@@ -4541,6 +5244,18 @@
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
"optional": true
},
+ "node_modules/jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/json-2-csv": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.0.1.tgz",
@@ -5458,6 +6173,14 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -6900,6 +7623,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -6955,6 +7687,14 @@
"node": ">=0.6.x"
}
},
+ "node_modules/tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+ "engines": {
+ "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+ }
+ },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
diff --git a/package.json b/package.json
index db7e33fdd..8ab1aa7be 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"@aws-sdk/client-secrets-manager": "^3.454.0",
"@aws-sdk/client-ses": "^3.454.0",
"@aws-sdk/credential-provider-node": "^3.451.0",
+ "@azure/storage-blob": "^12.17.0",
"@opensearch-project/opensearch": "^2.4.0",
"aws4": "^1.12.0",
"axios": "^1.6.2",
@@ -62,6 +63,7 @@
"xmlbuilder2": "^3.1.1"
},
"devDependencies": {
+ "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2",
"source-map-explorer": "^2.5.2"
}