Merge branch 'release/2024-03-01' into rome/release/2024-03-01

This commit is contained in:
Patrick Fic
2024-03-04 08:13:30 -05:00
34 changed files with 2706 additions and 635 deletions

16
.prettierrc.js Normal file
View File

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

View File

@@ -3,6 +3,15 @@
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports. modify the graphQL query and provide the user more power over their reports.
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 ## High level Schema Overview
```javascript ```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. Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server.
A note on special notation used in the `name` field. A note on special notation used in the `name` field.
## Reflection
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
```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 ### Path without brackets, multi level
`"name": "jobs.joblines.mod_lb_hrs",` `"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 ### Path with brackets,top level
`"name": "[jobs].joblines.mod_lb_hrs",` `"name": "[jobs].joblines.mod_lb_hrs",`
This will produce a where clause at the `jobs` level of the graphQL query. This will produce a where clause at the `jobs` level of the graphQL query.
@@ -107,14 +148,37 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
``` ```
## Known Caveats ## 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. - Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
- The type object must be 'string' or 'number' and is case-sensitive. 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. - 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. - Do not add the ability to filter on things like FK constraints, must like the above example.
## Sorters ## Sorters
- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level.
- 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"
}
}
```

View File

@@ -3,10 +3,22 @@ import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd"; import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { DELETE_BILL } from "../../graphql/bills.queries"; 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"; 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 [loading, setLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL); const [deleteBill] = useMutation(DELETE_BILL);
@@ -36,6 +48,10 @@ export default function BillDeleteButton({ bill, callback }) {
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({ message: t("bills.successes.deleted") }); notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
});
if (callback && typeof callback === "function") callback(bill.id); if (callback && typeof callback === "function") callback(bill.id);
} else { } else {

View File

@@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort, dateSort } from "../../utils/sorters";
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
@@ -58,7 +58,7 @@ export function BillsListTableComponent({
<EditFilled /> <EditFilled />
</Button> </Button>
)} )}
<BillDeleteButton bill={record} /> <BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent <BillDetailEditReturnComponent
data={{ bills_by_pk: { ...record, jobid: job.id } }} data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={ disabled={

View File

@@ -68,6 +68,30 @@ export default function ContractFormComponent({
<FormDateTimePicker /> <FormDateTimePicker />
</Form.Item> </Form.Item>
)} )}
{create && (
<Form.Item
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
>
{() => {
const insuranceOver =
selectedCar &&
selectedCar.insuranceexpires &&
moment(selectedCar.insuranceexpires)
.endOf("day")
.isBefore(moment(form.getFieldValue("scheduledreturn")));
if (insuranceOver)
return (
<Space direction="vertical" style={{ color: "tomato" }}>
<span>
<WarningFilled style={{ marginRight: ".3rem" }} />
{t("contracts.labels.insuranceexpired")}
</span>
</Space>
);
return <></>;
}}
</Form.Item>
)}
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow grow> <LayoutFormRow grow>
<Form.Item <Form.Item
@@ -90,16 +114,17 @@ export default function ContractFormComponent({
> >
{() => { {() => {
const mileageOver = const mileageOver =
selectedCar && selectedCar && selectedCar.nextservicekm
selectedCar.nextservicekm <= form.getFieldValue("kmstart"); ? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
: false;
const dueForService = const dueForService =
selectedCar && selectedCar &&
selectedCar.nextservicedate && selectedCar.nextservicedate &&
moment(selectedCar.nextservicedate).isBefore( moment(selectedCar.nextservicedate)
moment(form.getFieldValue("scheduledreturn")) .endOf("day")
); .isSameOrBefore(
moment(form.getFieldValue("scheduledreturn"))
);
if (mileageOver || dueForService) if (mileageOver || dueForService)
return ( return (
<Space direction="vertical" style={{ color: "tomato" }}> <Space direction="vertical" style={{ color: "tomato" }}>
@@ -117,7 +142,6 @@ export default function ContractFormComponent({
</span> </span>
</Space> </Space>
); );
return <></>; return <></>;
}} }}
</Form.Item> </Form.Item>

View File

@@ -17,13 +17,18 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({ loading, courtesycars, refetch }) { export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" },
}); });
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const [filter, setFilter] = useLocalStorage(
"filter_courtesy_cars_list",
null
);
const { t } = useTranslation(); const { t } = useTranslation();
const columns = [ const columns = [
@@ -50,6 +55,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
filteredValue: filter?.status || null,
filters: [ filters: [
{ {
text: t("courtesycars.status.in"), text: t("courtesycars.status.in"),
@@ -72,7 +78,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
const { nextservicedate, nextservicekm, mileage } = record; const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
@@ -80,11 +87,25 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
nextservicedate && nextservicedate &&
moment(nextservicedate).endOf("day").isSameOrBefore(moment()); moment(nextservicedate).endOf("day").isSameOrBefore(moment());
const insuranceOver =
insuranceexpires &&
moment(insuranceexpires).endOf("day").isBefore(moment());
return ( return (
<Space> <Space>
{t(record.status)} {t(record.status)}
{(mileageOver || dueForService) && ( {(mileageOver || dueForService || insuranceOver) && (
<Tooltip title={t("contracts.labels.cardueforservice")}> <Tooltip
title={
(mileageOver || dueForService) && insuranceOver
? t("contracts.labels.insuranceexpired") +
" / " +
t("contracts.labels.cardueforservice")
: insuranceOver
? t("contracts.labels.insuranceexpired")
: t("contracts.labels.cardueforservice")
}
>
<WarningFilled style={{ color: "tomato" }} /> <WarningFilled style={{ color: "tomato" }} />
</Tooltip> </Tooltip>
)} )}
@@ -97,6 +118,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
dataIndex: "readiness", dataIndex: "readiness",
key: "readiness", key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness), sorter: (a, b) => alphaSort(a.readiness, b.readiness),
filteredValue: filter?.readiness || null,
filters: [ filters: [
{ {
text: t("courtesycars.readiness.ready"), text: t("courtesycars.readiness.ready"),
@@ -212,7 +234,8 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); setState({ ...state, sortedInfo: sorter });
setFilter(filters);
}; };
const tableData = searchText const tableData = searchText

View File

@@ -3,21 +3,32 @@ import {
ExclamationCircleFilled, ExclamationCircleFilled,
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd"; import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component"; import DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledInToday({ data, ...cardProps }) { export default function DashboardScheduledInToday({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {},
}); });
const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage(
"isTvModeScheduledIn",
false
);
if (!data) return null; if (!data) return null;
if (!data.scheduled_in_today) if (!data.scheduled_in_today)
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
@@ -31,6 +42,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
alt_transport: item.job.alt_transport, alt_transport: item.job.alt_transport,
clm_no: item.job.clm_no, clm_no: item.job.clm_no,
jobid: item.job.jobid, jobid: item.job.jobid,
joblines_body: item.job.joblines
.filter((l) => l.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
joblines_ref: item.job.joblines
.filter((l) => l.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
ins_co_nm: item.job.ins_co_nm, ins_co_nm: item.job.ins_co_nm,
iouparent: item.job.iouparent, iouparent: item.job.iouparent,
ownerid: item.job.ownerid, ownerid: item.job.ownerid,
@@ -49,7 +66,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
v_vin: item.job.v_vin, v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid, vehicleid: item.job.vehicleid,
note: item.note, note: item.note,
start: moment(item.start).format("hh:mm a"), start: item.start,
title: item.title, title: item.title,
}; };
appt.push(i); appt.push(i);
@@ -59,11 +76,192 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
return new moment(a.start) - new moment(b.start); return new moment(a.start) - new moment(b.start);
}); });
const columns = [ const tvFontSize = 16;
const tvFontWeight = "bold";
const tvColumns = [
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
sorter: (a, b) => dateSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<TimeFormatter>{record.start}</TimeFormatter>
</span>
),
},
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</span>
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
},
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.alt_transport}
</span>
),
},
{
title: t("jobs.fields.lab"),
dataIndex: "joblines_body",
key: "joblines_body",
sorter: (a, b) => a.joblines_body - b.joblines_body,
sortOrder:
state.sortedInfo.columnKey === "joblines_body" &&
state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_body.toFixed(1)}
</span>
),
},
{
title: t("jobs.fields.lar"),
dataIndex: "joblines_ref",
key: "joblines_ref",
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
sortOrder:
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_ref.toFixed(1)}
</span>
),
},
];
const columns = [
{
title: t("appointments.fields.time"),
dataIndex: "start",
key: "start",
ellipsis: true,
sorter: (a, b) => dateSort(a.start, b.start),
sortOrder:
state.sortedInfo.columnKey === "start" && state.sortedInfo.order,
render: (text, record) => <TimeFormatter>{record.start}</TimeFormatter>,
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.jobid} to={"/manage/jobs/" + record.jobid}
@@ -91,7 +289,10 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
responsive: ["md"], sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link <Link
@@ -108,23 +309,16 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
}, },
}, },
{ {
title: t("jobs.fields.ownr_ph1"), title: t("dashboard.labels.phone"),
dataIndex: "ownr_ph1", dataIndex: "ownr_ph",
key: "ownr_ph1", key: "ownr_ph",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} /> <Space size="small" wrap>
), <ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
}, <ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
{ </Space>
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
), ),
}, },
{ {
@@ -134,7 +328,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} /> <a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
), ),
}, },
{ {
@@ -142,6 +336,15 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -165,43 +368,80 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
}, sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
{ sortOrder:
title: t("appointments.fields.time"), state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
dataIndex: "start", filters:
key: "start", (appt &&
ellipsis: true, appt
responsive: ["md"], .map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
}, },
{ {
title: t("appointments.fields.alt_transport"), title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport", dataIndex: "alt_transport",
key: "alt_transport", key: "alt_transport",
ellipsis: true, ellipsis: true,
responsive: ["md"], sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(appt &&
appt
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
}, },
]; ];
const handleTableChange = (sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<Card <Card
title={t("dashboard.titles.scheduledintoday", { title={t("dashboard.titles.scheduledindate", {
date: moment().startOf("day").format("MM/DD/YYYY"), date: moment().startOf("day").format("MM/DD/YYYY"),
})} })}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledIn(!isTvModeScheduledIn)}
defaultChecked={isTvModeScheduledIn}
/>
</Space>
}
{...cardProps} {...cardProps}
> >
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={false}
columns={columns} columns={isTvModeScheduledIn ? tvColumns : columns}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}
dataSource={appt} dataSource={appt}
size={isTvModeScheduledIn ? "small" : "middle"}
/> />
</div> </div>
</Card> </Card>
@@ -220,6 +460,10 @@ export const DashboardScheduledInTodayGql = `
alt_transport alt_transport
clm_no clm_no
jobid: id jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm ins_co_nm
iouparent iouparent
ownerid ownerid

View File

@@ -3,37 +3,272 @@ import {
ExclamationCircleFilled, ExclamationCircleFilled,
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd"; import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TimeFormatter } from "../../../utils/DateFormatter";
import { onlyUnique } from "../../../utils/arrayHelper";
import { alphaSort, dateSort } from "../../../utils/sorters";
import useLocalStorage from "../../../utils/useLocalStorage";
import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../../owner-name-display/owner-name-display.component";
import DashboardRefreshRequired from "../refresh-required.component"; import DashboardRefreshRequired from "../refresh-required.component";
import {pageLimit} from "../../../utils/config";
export default function DashboardScheduledOutToday({ data, ...cardProps }) { export default function DashboardScheduledOutToday({ data, ...cardProps }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {},
}); });
const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage(
"isTvModeScheduledOut",
false
);
if (!data) return null; if (!data) return null;
if (!data.scheduled_out_today) if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
data.scheduled_out_today.forEach((item) => { data.scheduled_out_today.forEach((item) => {
item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a") item.joblines_body = item.joblines
? item.joblines
.filter((l) => l.mod_lbr_ty !== "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
item.joblines_ref = item.joblines
? item.joblines
.filter((l) => l.mod_lbr_ty === "LAR")
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
: 0;
}); });
data.scheduled_out_today.sort(function (a, b) { data.scheduled_out_today.sort(function (a, b) {
return new Date(a.scheduled_completion) - new Date(b.scheduled_completion); return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
}); });
const columns = [ const tvFontSize = 18;
const tvFontWeight = "bold";
const tvColumns = [
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
</span>
),
},
{ {
title: t("jobs.fields.ro_number"), title: t("jobs.fields.ro_number"),
dataIndex: "ro_number", dataIndex: "ro_number",
key: "ro_number", key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.jobid}
onClick={(e) => e.stopPropagation()}
>
<Space>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</span>
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.ownerid ? (
<Link
to={"/manage/owners/" + record.ownerid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</span>
</Link>
) : (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
record.v_model_yr || ""
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
);
},
},
{
title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport",
key: "alt_transport",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.alt_transport}
</span>
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.status),
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.status}
</span>
),
},
{
title: t("jobs.fields.lab"),
dataIndex: "joblines_body",
key: "joblines_body",
sorter: (a, b) => a.joblines_body - b.joblines_body,
sortOrder:
state.sortedInfo.columnKey === "joblines_body" &&
state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_body.toFixed(1)}
</span>
),
},
{
title: t("jobs.fields.lar"),
dataIndex: "joblines_ref",
key: "joblines_ref",
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
sortOrder:
state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
align: "right",
render: (text, record) => (
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
{record.joblines_ref.toFixed(1)}
</span>
),
},
];
const columns = [
{
title: t("jobs.fields.scheduled_completion"),
dataIndex: "scheduled_completion",
key: "scheduled_completion",
ellipsis: true,
sorter: (a, b) =>
dateSort(a.scheduled_completion, b.scheduled_completion),
sortOrder:
state.sortedInfo.columnKey === "scheduled_completion" &&
state.sortedInfo.order,
render: (text, record) => (
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
),
},
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => ( render: (text, record) => (
<Link <Link
to={"/manage/jobs/" + record.jobid} to={"/manage/jobs/" + record.jobid}
@@ -61,7 +296,10 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "owner", dataIndex: "owner",
key: "owner", key: "owner",
ellipsis: true, ellipsis: true,
responsive: ["md"], sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.ownerid ? ( return record.ownerid ? (
<Link <Link
@@ -78,23 +316,16 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
}, },
}, },
{ {
title: t("jobs.fields.ownr_ph1"), title: t("dashboard.labels.phone"),
dataIndex: "ownr_ph1", dataIndex: "ownr_ph",
key: "ownr_ph1", key: "ownr_ph",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} /> <Space size="small" wrap>
), <ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
}, <ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
{ </Space>
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
), ),
}, },
{ {
@@ -104,7 +335,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
render: (text, record) => ( render: (text, record) => (
<ChatOpenButton phone={record.ownr_ea} jobid={record.jobid} /> <a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
), ),
}, },
{ {
@@ -112,6 +343,15 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
dataIndex: "vehicle", dataIndex: "vehicle",
key: "vehicle", key: "vehicle",
ellipsis: true, ellipsis: true,
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
return record.vehicleid ? ( return record.vehicleid ? (
<Link <Link
@@ -135,43 +375,80 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
key: "ins_co_nm", key: "ins_co_nm",
ellipsis: true, ellipsis: true,
responsive: ["md"], responsive: ["md"],
}, sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
{ sortOrder:
title: t("jobs.fields.scheduled_completion"), state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
dataIndex: "scheduled_completion", filters:
key: "scheduled_completion", (data.scheduled_out_today &&
ellipsis: true, data.scheduled_out_today
responsive: ["md"], .map((j) => j.ins_co_nm)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Ins. Co.*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.ins_co_nm),
}, },
{ {
title: t("appointments.fields.alt_transport"), title: t("appointments.fields.alt_transport"),
dataIndex: "alt_transport", dataIndex: "alt_transport",
key: "alt_transport", key: "alt_transport",
ellipsis: true, ellipsis: true,
responsive: ["md"], sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
filters:
(data.scheduled_out_today &&
data.scheduled_out_today
.map((j) => j.alt_transport)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Alt. Transport*",
value: [s],
};
})
.sort((a, b) => alphaSort(a.text, b.text))) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),
}, },
]; ];
const handleTableChange = (sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, sortedInfo: sorter }); setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
}; };
return ( return (
<Card <Card
title={t("dashboard.titles.scheduledouttoday", { title={t("dashboard.titles.scheduledoutdate", {
date: moment().startOf("day").format("MM/DD/YYYY"), date: moment().startOf("day").format("MM/DD/YYYY"),
})} })}
extra={
<Space>
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
<Switch
onClick={() => setIsTvModeScheduledOut(!isTvModeScheduledOut)}
defaultChecked={isTvModeScheduledOut}
/>
</Space>
}
{...cardProps} {...cardProps}
> >
<div style={{ height: "100%" }}> <div style={{ height: "100%" }}>
<Table <Table
onChange={handleTableChange} onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: pageLimit }} pagination={false}
columns={columns} columns={isTvModeScheduledOut ? tvColumns : columns}
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}
dataSource={data.scheduled_out_today} dataSource={data.scheduled_out_today}
size={isTvModeScheduledOut ? "small" : "middle"}
/> />
</div> </div>
</Card> </Card>
@@ -188,6 +465,10 @@ export const DashboardScheduledOutTodayGql = `
alt_transport alt_transport
clm_no clm_no
jobid: id jobid: id
joblines(where: {removed: {_eq: false}}) {
mod_lb_hrs
mod_lbr_ty
}
ins_co_nm ins_co_nm
iouparent iouparent
ownerid ownerid
@@ -200,6 +481,7 @@ export const DashboardScheduledOutTodayGql = `
production_vars production_vars
ro_number ro_number
scheduled_completion scheduled_completion
status
suspended suspended
v_make_desc v_make_desc
v_model_desc v_model_desc

View File

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

View File

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

View File

@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient"; import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
function updateJobCache(items) { function updateJobCache(items) {
client.cache.modify({ client.cache.modify({
id: "ROOT_QUERY", id: "ROOT_QUERY",
@@ -40,6 +47,7 @@ export function JobsCloseExportButton({
disabled, disabled,
setSelectedJobs, setSelectedJobs,
refetch, refetch,
insertAuditTrail,
}) { }) {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -181,6 +189,10 @@ export function JobsCloseExportButton({
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
});
updateJobCache( updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id) 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({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
});
updateJobCache([ updateJobCache([
...new Set( ...new Set(
successfulTransactions.map( successfulTransactions.map(
@@ -227,4 +247,7 @@ export function JobsCloseExportButton({
); );
} }
export default connect(mapStateToProps, null)(JobsCloseExportButton); export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsCloseExportButton);

View File

@@ -132,6 +132,12 @@ export function JobsDetailHeaderActions({
}, },
}, },
}); });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobsuspend(
!!job.suspended ? !job.suspended : true
),
});
}; };
const statusmenu = ( const statusmenu = (
@@ -561,6 +567,10 @@ export function JobsDetailHeaderActions({
notification["success"]({ notification["success"]({
message: t("jobs.successes.voided"), message: t("jobs.successes.voided"),
}); });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobvoid(),
});
//go back to jobs list. //go back to jobs list.
history.push(`/manage/`); history.push(`/manage/`);
} else { } else {

View File

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

View File

@@ -9,10 +9,12 @@ import { createStructuredSelector } from "reselect";
import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_JOBS } from "../../graphql/jobs.queries"; import { UPDATE_JOBS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient"; import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
function updateJobCache(items) { function updateJobCache(items) {
client.cache.modify({ client.cache.modify({
id: "ROOT_QUERY", id: "ROOT_QUERY",
@@ -41,6 +48,7 @@ export function JobsExportAllButton({
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch, refetch,
insertAuditTrail,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS); const [updateJob] = useMutation(UPDATE_JOBS);
@@ -177,6 +185,12 @@ export function JobsExportAllButton({
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobexported(),
});
});
updateJobCache( updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map( jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id (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({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
updateJobCache([ const successfulTransactionsSet = [
...new Set( ...new Set(
successfulTransactions.map( successfulTransactions.map(
(st) => (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);

View File

@@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
await insertPayment({ await insertPayment({
variables: { variables: {
paymentInput: { paymentInput: {
amount: -refund_response.data.amount, amount: -refund_response?.data?.amount,
transactionid: payment_response.response.receiptelements.transid, transactionid: payment_response?.response?.receiptelements?.transid,
payer: record.payer, payer: record.payer,
type: "Refund", type: "Refund",
jobid: payment_response.jobid, jobid: payment_response.jobid,

View File

@@ -1,52 +1,414 @@
import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd"; import {Button, Card, Checkbox, Col, Form, Input, InputNumber, Row, Select} from "antd";
import React, {useEffect, useState} from "react"; import React, {useCallback, useEffect, useMemo, useState} from "react";
import {fetchFilterData} from "../../utils/RenderTemplate"; import {fetchFilterData} from "../../utils/RenderTemplate";
import {DeleteFilled} from "@ant-design/icons"; import {DeleteFilled} from "@ant-design/icons";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {getOperatorsByType} from "../../utils/graphQLmodifier"; import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import {generateInternalReflections} from "./report-center-modal-utils";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({form}) { export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
return ( return (
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}> <Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => { {() => {
const key = form.getFieldValue("key"); const key = form.getFieldValue("key");
return <RenderFilters form={form} templateId={key}/>; return <RenderFilters form={form} templateId={key} bodyshop={bodyshop}/>;
}} }}
</Form.Item> </Form.Item>
); );
} }
function RenderFilters({templateId, form}) { /**
* Filters Section
* @param filters
* @param form
* @param bodyshop
* @returns {JSX.Element}
* @constructor
*/
function FiltersSection({filters, form, bodyshop}) {
const {t} = useTranslation();
return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
<Form.List name={["filters"]}>
{(fields, {add, remove}) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={10}>
<Form.Item
key={`${index}field`}
label={t('reportcenter.labels.advanced_filters_filter_field')}
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
getPopupContainer={trigger => trigger.parentNode}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], null);
form.setFieldValue(['filters', field.name, 'operator'], null);
}}
options={
filters.map((f) => {
return {
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}
})
}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
dependencies={[['filters', field.name, "field"],['filters', field.name, "value"]]}
>
{
() => {
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = filters.find(f => f.name === name)?.type;
return <Form.Item
key={`${index}operator`}
label={t('reportcenter.labels.advanced_filters_filter_operator')}
name={[field.name, "operator"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
getPopupContainer={trigger => trigger.parentNode}
options={ getWhereOperatorsByType(type)}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], undefined);
}}
/>
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={6}>
<Form.Item dependencies={[
['filters', field.name, "field"],
['filters', field.name, "operator"]
]}
>
{
() => {
// 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 <Form.Item
key={`${index}value`}
label={t('reportcenter.labels.advanced_filters_filter_value')}
name={[field.name, "value"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
{
(() => {
const generateReflections = (reflector) => {
if (!reflector) return [];
const {name} = reflector;
const path = name?.split('.');
const upperPath = path?.[0];
const finalPath = path?.slice(1).join('.');
return generateInternalReflections({
bodyshop,
upperPath,
finalPath
});
};
const reflections = reflector ? generateReflections(reflector) : [];
const fieldPath = [[field.name, "value"]];
// 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 (
<Select
disabled={!operator}
mode="multiple"
options={reflections}
getPopupContainer={trigger => trigger.parentNode}
onChange={(value) => {
form.setFieldValue(fieldPath, value);
}}
/>
);
}
return (
<Select
options={reflections}
getPopupContainer={trigger => 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 (
<InputNumber
disabled={!operator}
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
);
}
// We have a type of date, so we will use a date picker
if (type === "date") {
return (
<FormDatePicker
disabled={!operator}
onChange={(date) => 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 (
<Select
disabled={!operator}
getPopupContainer={trigger => 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 (
<Input
disabled={!operator}
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}
/>
);
})()
}
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem", paddingTop: '23px'}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
);
}
/**
* Sorters Section
* @param sorters
* @param form
* @returns {JSX.Element}
* @constructor
*/
function SortersSection({sorters}) {
const {t} = useTranslation();
return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
<Form.List name={["sorters"]}>
{(fields, {add, remove}) => {
return (
<div>
Sorters
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={11}>
<Form.Item
key={`${index}field`}
label={t('reportcenter.labels.advanced_filters_sorter_field')}
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={
sorters.map((f) => ({
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}))
}
getPopupContainer={trigger => trigger.parentNode}
/>
</Form.Item>
</Col>
<Col span={11}>
<Form.Item
key={`${index}direction`}
label={t('reportcenter.labels.advanced_filters_sorter_direction')}
name={[field.name, "direction"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={getOrderOperatorsByType()}
getPopupContainer={trigger => trigger.parentNode}
/>
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem", paddingTop: '23px'}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
);
}
/**
* Render Filters
* @param templateId
* @param form
* @param bodyshop
* @returns {JSX.Element|null}
* @constructor
*/
function RenderFilters({templateId, form, bodyshop}) {
const [state, setState] = useState(null); const [state, setState] = useState(null);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const {t} = useTranslation(); const {t} = useTranslation();
useEffect(() => { const fetch = useCallback(async () => {
const fetch = async () => { // Reset all the filters and Sorters.
setIsLoading(true); form.resetFields(['filters']);
const data = await fetchFilterData({name: templateId}); form.resetFields(['sorters']);
if (data?.success) { form.resetFields(['defaultSorters']);
setState(data.data);
} else {
setState(null);
}
setIsLoading(false);
};
setIsLoading(true);
const data = await fetchFilterData({name: templateId});
// We have Success
if (data?.success) {
if (data?.data?.sorters && data?.data?.sorters.length > 0) {
const defaultSorters = data?.data?.sorters.filter((sorter) => sorter.hasOwnProperty('default')).map((sorter) => {
return {
field: sorter.name,
direction: sorter.default.direction
};
}).sort((a, b) => a.default.order - b.default.order);
form.setFieldValue('defaultSorters', JSON.stringify(defaultSorters));
}
// Set the state
setState(data.data);
}
// Something went wrong fetching filter data
else {
setState(null);
}
setIsLoading(false);
}, [templateId, form]);
useEffect(() => {
if (templateId) { if (templateId) {
fetch(); fetch();
} }
}, [templateId]); }, [templateId, fetch]);
const filters = useMemo(() => state?.filters || [], [state]);
const sorters = useMemo(() => state?.sorters || [], [state]);
// Conditional display of filters and sorters
if (!templateId) return null; if (!templateId) return null;
if (isLoading) return <LoadingSkeleton/>; if (isLoading) return <LoadingSkeleton/>;
if (!state) return null; if (!state) return null;
// Filters and Sorters data available
return ( return (
<div style={{marginTop: '10px'}}> <div style={{marginTop: '10px'}}>
<Checkbox <Checkbox
@@ -56,215 +418,11 @@ function RenderFilters({templateId, form}) {
/> />
{visible && ( {visible && (
<div> <div>
{state.filters && state.filters.length > 0 && ( {filters.length > 0 && (
<Card type='inner' title={ t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}> <FiltersSection filters={filters} form={form} bodyshop={bodyshop}/>
<Form.List name={["filters"]}>
{(fields, {add, remove, move}) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={10}>
<Form.Item
key={`${index}field`}
label="field"
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={
state.filters
? state.filters.map((f) => {
return {
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}
})
: []
}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item dependencies={[['filters', field.name, "field"]]}>
{
() => {
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = state.filters.find(f => f.name === name)?.type;
return <Form.Item
key={`${index}operator`}
label="operator"
name={[field.name, "operator"]}
dependencies={[]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select options={getOperatorsByType(type)}/>
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={6}>
<Form.Item dependencies={[['filters', field.name, "field"]]}>
{
() => {
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = state.filters.find(f => f.name === name)?.type;
return <Form.Item
key={`${index}value`}
label="value"
name={[field.name, "value"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
{type === 'number' ?
<InputNumber
onChange={(value) => {
form.setFieldsValue({[field.name]: {value: parseInt(value)}});
}}
/>
:
<Input
onChange={(value) => {
form.setFieldsValue({[field.name]: {value: value.toString()}});
}}
/>
}
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem"}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
)} )}
{state.sorters && state.sorters.length > 0 && ( {sorters.length > 0 && (
<Card type='inner' title={ t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}> <SortersSection sorters={sorters} form={form}/>
<Form.List name={["sorters"]}>
{(fields, {add, remove, move}) => {
return (
<div>
Sorters
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Row gutter={[16, 16]}>
<Col span={11}>
<Form.Item
key={`${index}field`}
label="field"
name={[field.name, "field"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={
state.sorters
? state.sorters.map((f) => ({
value: f.name,
label: f?.translation ? (t(f.translation) === f.translation ? f.label : t(f.translation)) : f.label,
}))
: []
}
/>
</Form.Item>
</Col>
<Col span={11}>
<Form.Item
key={`${index}direction`}
label="direction"
name={[field.name, "direction"]}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Select
options={[
{value: "desc", label: "Descending"},
{value: "asc", label: "Ascending"},
]}
/>
</Form.Item>
</Col>
<Col span={2}>
<DeleteFilled
style={{margin: "1rem"}}
onClick={() => {
remove(field.name);
}}
/>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{width: "100%"}}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Card>
)} )}
</div> </div>
)} )}

View File

@@ -0,0 +1,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}

View File

@@ -18,10 +18,11 @@ import EmployeeSearchSelect from "../employee-search-select/employee-search-sele
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import "./report-center-modal.styles.scss"; import "./report-center-modal.styles.scss";
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component"; import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
import {selectBodyshop} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
reportCenterModal: selectReportCenter, reportCenterModal: selectReportCenter,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -89,22 +90,28 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
const end = values.dates ? values.dates[1] : null; const end = values.dates ? values.dates[1] : null;
const { id } = values; const { id } = values;
await GenerateDocument( const templateConfig = {
{
name: values.key, name: values.key,
variables: { variables: {
...(start ...(start
? { start: moment(start).startOf("day").format("YYYY-MM-DD") } ? {start: moment(start).startOf("day").format("YYYY-MM-DD")}
: {}), : {}),
...(end ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } : {}), ...(end ? {end: moment(end).endOf("day").format("YYYY-MM-DD")} : {}),
...(start ? { starttz: moment(start).startOf("day") } : {}), ...(start ? {starttz: moment(start).startOf("day")} : {}),
...(end ? { endtz: moment(end).endOf("day") } : {}), ...(end ? {endtz: moment(end).endOf("day")} : {}),
...(id ? { id: id } : {}), ...(id ? {id: id} : {}),
}, },
filters: values.filters, filters: values.filters,
sorters: values.sorters, sorters: values.sorters,
}, };
if (_.isString(values.defaultSorters) && !_.isEmpty(values.defaultSorters)) {
templateConfig.defaultSorters = JSON.parse(values.defaultSorters);
}
await GenerateDocument(
templateConfig,
{ {
to: values.to, to: values.to,
subject: Templates[values.key]?.subject, subject: Templates[values.key]?.subject,
@@ -142,7 +149,8 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
value={search} value={search}
/> />
<Form.Item <Form.Item name="defaultSorters" hidden/>
<Form.Item
name="key" name="key"
label={t("reportcenter.labels.key")} label={t("reportcenter.labels.key")}
// className="radio-group-columns" // className="radio-group-columns"
@@ -206,7 +214,7 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
); );
}} }}
</Form.Item> </Form.Item>
<ReportCenterModalFiltersSortersComponent form={form} /> <ReportCenterModalFiltersSortersComponent form={form} bodyshop={bodyshop} />
<Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}> <Form.Item style={{margin: 0, padding: 0}} dependencies={["key"]}>
{() => { {() => {
const key = form.getFieldValue("key"); const key = form.getFieldValue("key");
@@ -261,6 +269,9 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
{() => { {() => {
const key = form.getFieldValue("key"); const key = form.getFieldValue("key");
const datedisable = Templates[key] && Templates[key].datedisable; const datedisable = Templates[key] && Templates[key].datedisable;
// TODO: MERGE NOTE, Ranges turns to presets in DatePicker.RangePicker
if (datedisable !== true) { if (datedisable !== true) {
return ( return (
<Form.Item <Form.Item
@@ -275,7 +286,7 @@ export function ReportCenterModalComponent({reportCenterModal, bodyshop}) {
> >
<DatePicker.RangePicker <DatePicker.RangePicker
format="MM/DD/YYYY" format="MM/DD/YYYY"
presets={DatePickerRanges} ranges={DatePickerRanges}
/> />
</Form.Item> </Form.Item>
); );

View File

@@ -25,6 +25,8 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
return acc + value.bodyhrs; return acc + value.bodyhrs;
}, 0); }, 0);
const numJobs = entries.length;
return ( return (
<Card <Card
title={moment(date).format("D - ddd")} title={moment(date).format("D - ddd")}
@@ -33,17 +35,18 @@ export function ScoreboardDayStats({ bodyshop, date, entries }) {
> >
<Statistic <Statistic
valueStyle={{ color: dailyBodyTarget > bodyHrs ? "red" : "green" }} valueStyle={{ color: dailyBodyTarget > bodyHrs ? "red" : "green" }}
label="B" label="Body"
value={bodyHrs.toFixed(1)} value={bodyHrs.toFixed(1)}
/> />
<Statistic <Statistic
valueStyle={{ color: dailyPaintTarget > paintHrs ? "red" : "green" }} valueStyle={{ color: dailyPaintTarget > paintHrs ? "red" : "green" }}
label="P" label="Refinish"
value={paintHrs.toFixed(1)} value={paintHrs.toFixed(1)}
/> />
<Divider style={{ margin: 0 }} /> <Divider style={{ margin: 0 }} />
<Statistic label="Total" value={(bodyHrs + paintHrs).toFixed(1)} />
<Statistic value={(bodyHrs + paintHrs).toFixed(1)} /> <Divider style={{ margin: 0 }} />
<Statistic label="Jobs" value={numJobs} />
</Card> </Card>
); );
} }

View File

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

View File

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

View File

@@ -29,10 +29,13 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
let ret = { let ret = {
todayBody: 0, todayBody: 0,
todayPaint: 0, todayPaint: 0,
todayJobs: 0,
weeklyPaint: 0, weeklyPaint: 0,
weeklyJobs: 0,
weeklyBody: 0, weeklyBody: 0,
toDateBody: 0, toDateBody: 0,
toDatePaint: 0, toDatePaint: 0,
toDateJobs: 0,
}; };
const today = moment(); const today = moment();
@@ -40,6 +43,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[today.format("YYYY-MM-DD")].forEach((d) => { dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
ret.todayBody = ret.todayBody + d.bodyhrs; ret.todayBody = ret.todayBody + d.bodyhrs;
ret.todayPaint = ret.todayPaint + d.painthrs; 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) => { dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
ret.weeklyBody = ret.weeklyBody + d.bodyhrs; ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
ret.weeklyPaint = ret.weeklyPaint + d.painthrs; ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
ret.weeklyJobs++;
}); });
} }
StartOfWeek = StartOfWeek.add(1, "day"); StartOfWeek = StartOfWeek.add(1, "day");
@@ -60,6 +65,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => { dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
ret.toDateBody = ret.toDateBody + d.bodyhrs; ret.toDateBody = ret.toDateBody + d.bodyhrs;
ret.toDatePaint = ret.toDatePaint + d.painthrs; ret.toDatePaint = ret.toDatePaint + d.painthrs;
ret.toDateJobs++;
}); });
} }
startOfMonth = startOfMonth.add(1, "day"); startOfMonth = startOfMonth.add(1, "day");
@@ -87,7 +93,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Statistic <Statistic
title={t("scoreboard.labels.dailytarget")} title={t("scoreboard.labels.dailytarget")}
value={bodyshop.scoreboard_target.dailyBodyTarget} value={bodyshop.scoreboard_target.dailyBodyTarget}
prefix="B" prefix={t("scoreboard.labels.bodyabbrev")}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
@@ -140,7 +146,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={bodyshop.scoreboard_target.dailyPaintTarget} value={bodyshop.scoreboard_target.dailyPaintTarget}
prefix="P" prefix={t("scoreboard.labels.refinishabbrev")}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
@@ -181,7 +187,12 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
<Divider style={{ margin: 5 }} /> <Divider style={{ margin: 5 }} />
</Row> </Row>
<Row> <Row>
<Col {...statSpans}></Col> <Col {...statSpans}>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.total")}
/>
</Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={(values.todayPaint + values.todayBody).toFixed(1)} value={(values.todayPaint + values.todayBody).toFixed(1)}
@@ -240,6 +251,29 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
/> />
</Col> </Col>
</Row> </Row>
<Row>
<Divider style={{ margin: 5 }} />
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.jobs")}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.todayJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.weeklyJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.toDateJobs} />
</Col>
</Row>
</Col> </Col>
</Row> </Row>
</Card> </Card>

View File

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

View File

@@ -26,10 +26,12 @@ import { OwnerNameDisplayFunction } from "../../components/owner-name-display/ow
import { auth } from "../../firebase/firebase.utils"; import { auth } from "../../firebase/firebase.utils";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries"; import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import { import {
insertAuditTrail,
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -38,15 +40,17 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export const socket = SocketIO( export const socket = SocketIO(
// process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
// ? process.env.REACT_APP_AXIOS_BASE_API_URL ? process.env.REACT_APP_AXIOS_BASE_API_URL
// : window.location.origin, : window.location.origin,
"http://localhost:4000", // for dev testing, // "http://localhost:4000", // for dev testing,
{ {
path: "/ws", path: "/ws",
withCredentials: true, 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 { t } = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG"); const [logLevel, setLogLevel] = useState("DEBUG");
const history = useHistory(); const history = useHistory();
@@ -115,6 +124,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
notification.success({ notification.success({
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
});
history.push("/manage/accounting/receivables"); history.push("/manage/accounting/receivables");
}); });

View File

@@ -289,7 +289,7 @@ export function* insertAuditTrailSaga({
fields: { fields: {
audit_trail(existingAuditTrail, { readField }) { audit_trail(existingAuditTrail, { readField }) {
const newAuditTrail = cache.writeQuery({ const newAuditTrail = cache.writeQuery({
data: data.insert_audit_trail_one, data: data,
query: INSERT_AUDIT_TRAIL, query: INSERT_AUDIT_TRAIL,
variables, variables,
}); });

View File

@@ -109,6 +109,7 @@
"appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.", "appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.",
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.", "appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
"assignedlinehours": "Assigned job lines totaling {{hours}} units to {{team}}.", "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.", "billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.", "billupdated": "Bill with invoice number {{invoice_number}} updated.",
"failedpayment": "Failed payment attempt.", "failedpayment": "Failed payment attempt.",
@@ -116,6 +117,7 @@
"jobassignmentremoved": "Employee assignment removed for {{operation}}", "jobassignmentremoved": "Employee assignment removed for {{operation}}",
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.", "jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
"jobconverted": "Job converted and assigned number {{ro_number}}.", "jobconverted": "Job converted and assigned number {{ro_number}}.",
"jobexported": "Job has been exported.",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.", "jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.", "jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}", "jobinproductionchange": "Job production status set to {{inproduction}}",
@@ -128,7 +130,9 @@
"jobspartsorder": "Parts order {{order_number}} added to Job.", "jobspartsorder": "Parts order {{order_number}} added to Job.",
"jobspartsreturn": "Parts return {{order_number}} added to Job.", "jobspartsreturn": "Parts return {{order_number}} added to Job.",
"jobstatuschange": "Job status changed to {{status}}.", "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": { "billlines": {
@@ -760,6 +764,7 @@
"driverinformation": "Driver's Information", "driverinformation": "Driver's Information",
"findcontract": "Find Contract", "findcontract": "Find Contract",
"findermodal": "Contract Finder", "findermodal": "Contract Finder",
"insuranceexpired": "The courtesy car insurance expires before the car is expected to return.",
"noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.", "noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.",
"populatefromjob": "Populate from Job", "populatefromjob": "Populate from Job",
"rates": "Contract Rates", "rates": "Contract Rates",
@@ -887,6 +892,7 @@
"labels": { "labels": {
"bodyhrs": "Body Hrs", "bodyhrs": "Body Hrs",
"dollarsinproduction": "Dollars in Production", "dollarsinproduction": "Dollars in Production",
"phone": "Phone",
"prodhrs": "Production Hrs", "prodhrs": "Production Hrs",
"refhrs": "Refinish Hrs" "refhrs": "Refinish Hrs"
}, },
@@ -902,8 +908,10 @@
"productiondollars": "Total Dollars in Production", "productiondollars": "Total Dollars in Production",
"productionhours": "Total Hours in Production", "productionhours": "Total Hours in Production",
"projectedmonthlysales": "Projected Monthly Sales", "projectedmonthlysales": "Projected Monthly Sales",
"scheduledintoday": "Sheduled In Today: {{date}}", "scheduledindate": "Sheduled In Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today: {{date}}" "scheduledintoday": "Sheduled In Today",
"scheduledoutdate": "Sheduled Out Today: {{date}}",
"scheduledouttoday": "Sheduled Out Today"
} }
}, },
"dms": { "dms": {
@@ -2674,6 +2682,13 @@
"advanced_filters_hide": "Hide", "advanced_filters_hide": "Hide",
"advanced_filters_filters": "Filters", "advanced_filters_filters": "Filters",
"advanced_filters_sorters": "Sorters", "advanced_filters_sorters": "Sorters",
"advanced_filters_filter_field": "Field",
"advanced_filters_sorter_field": "Field",
"advanced_filters_true": "True",
"advanced_filters_false": "False",
"advanced_filters_sorter_direction": "Direction",
"advanced_filters_filter_operator": "Operator",
"advanced_filters_filter_value": "Value",
"dates": "Dates", "dates": "Dates",
"employee": "Employee", "employee": "Employee",
"filterson": "Filters on {{object}}: {{field}}", "filterson": "Filters on {{object}}: {{field}}",
@@ -2849,6 +2864,7 @@
"allemployeetimetickets": "All Employee Time Tickets", "allemployeetimetickets": "All Employee Time Tickets",
"asoftodaytarget": "As of Today", "asoftodaytarget": "As of Today",
"body": "Body", "body": "Body",
"bodyabbrev": "B",
"bodycharttitle": "Body Targets vs Actual", "bodycharttitle": "Body Targets vs Actual",
"calendarperiod": "Periods based on calendar weeks/months.", "calendarperiod": "Periods based on calendar weeks/months.",
"combinedcharttitle": "Combined Targets vs Actual", "combinedcharttitle": "Combined Targets vs Actual",
@@ -2865,6 +2881,7 @@
"productivestatistics": "Productive Hours Statistics", "productivestatistics": "Productive Hours Statistics",
"productivetimeticketsoverdate": "Productive Hours over Selected Dates", "productivetimeticketsoverdate": "Productive Hours over Selected Dates",
"refinish": "Refinish", "refinish": "Refinish",
"refinishabbrev": "R",
"refinishcharttitle": "Refinish Targets vs Actual", "refinishcharttitle": "Refinish Targets vs Actual",
"targets": "Targets", "targets": "Targets",
"thismonth": "This Month", "thismonth": "This Month",
@@ -2872,6 +2889,7 @@
"timetickets": "Time Tickets", "timetickets": "Time Tickets",
"timeticketsemployee": "Time Tickets by Employee", "timeticketsemployee": "Time Tickets by Employee",
"todateactual": "Actual (MTD)", "todateactual": "Actual (MTD)",
"total": "Total",
"totalhrs": "Total Hours", "totalhrs": "Total Hours",
"totaloverperiod": "Total over Selected Dates", "totaloverperiod": "Total over Selected Dates",
"weeklyactual": "Actual (W)", "weeklyactual": "Actual (W)",

View File

@@ -109,6 +109,7 @@
"appointmentcancel": "", "appointmentcancel": "",
"appointmentinsert": "", "appointmentinsert": "",
"assignedlinehours": "", "assignedlinehours": "",
"billdeleted": "",
"billposted": "", "billposted": "",
"billupdated": "", "billupdated": "",
"failedpayment": "", "failedpayment": "",
@@ -116,6 +117,7 @@
"jobassignmentremoved": "", "jobassignmentremoved": "",
"jobchecklist": "", "jobchecklist": "",
"jobconverted": "", "jobconverted": "",
"jobexported": "",
"jobfieldchanged": "", "jobfieldchanged": "",
"jobimported": "", "jobimported": "",
"jobinproductionchange": "", "jobinproductionchange": "",
@@ -128,7 +130,9 @@
"jobspartsorder": "", "jobspartsorder": "",
"jobspartsreturn": "", "jobspartsreturn": "",
"jobstatuschange": "", "jobstatuschange": "",
"jobsupplement": "" "jobsupplement": "",
"jobsuspend": "",
"jobvoid": ""
} }
}, },
"billlines": { "billlines": {
@@ -760,6 +764,7 @@
"driverinformation": "", "driverinformation": "",
"findcontract": "", "findcontract": "",
"findermodal": "", "findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "", "noteconvertedfrom": "",
"populatefromjob": "", "populatefromjob": "",
"rates": "", "rates": "",
@@ -887,6 +892,7 @@
"labels": { "labels": {
"bodyhrs": "", "bodyhrs": "",
"dollarsinproduction": "", "dollarsinproduction": "",
"phone": "",
"prodhrs": "", "prodhrs": "",
"refhrs": "" "refhrs": ""
}, },
@@ -902,7 +908,9 @@
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "", "projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "",
"scheduledouttoday": "" "scheduledouttoday": ""
} }
}, },
@@ -2674,6 +2682,13 @@
"advanced_filters_hide": "", "advanced_filters_hide": "",
"advanced_filters_filters": "", "advanced_filters_filters": "",
"advanced_filters_sorters": "", "advanced_filters_sorters": "",
"advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "",
"advanced_filters_true": "",
"advanced_filters_false": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",
@@ -2849,6 +2864,7 @@
"allemployeetimetickets": "", "allemployeetimetickets": "",
"asoftodaytarget": "", "asoftodaytarget": "",
"body": "", "body": "",
"bodyabbrev": "",
"bodycharttitle": "", "bodycharttitle": "",
"calendarperiod": "", "calendarperiod": "",
"combinedcharttitle": "", "combinedcharttitle": "",
@@ -2865,6 +2881,7 @@
"productivestatistics": "", "productivestatistics": "",
"productivetimeticketsoverdate": "", "productivetimeticketsoverdate": "",
"refinish": "", "refinish": "",
"refinishabbrev": "",
"refinishcharttitle": "", "refinishcharttitle": "",
"targets": "", "targets": "",
"thismonth": "", "thismonth": "",
@@ -2872,6 +2889,7 @@
"timetickets": "", "timetickets": "",
"timeticketsemployee": "", "timeticketsemployee": "",
"todateactual": "", "todateactual": "",
"total": "",
"totalhrs": "", "totalhrs": "",
"totaloverperiod": "", "totaloverperiod": "",
"weeklyactual": "", "weeklyactual": "",

View File

@@ -109,6 +109,7 @@
"appointmentcancel": "", "appointmentcancel": "",
"appointmentinsert": "", "appointmentinsert": "",
"assignedlinehours": "", "assignedlinehours": "",
"billdeleted": "",
"billposted": "", "billposted": "",
"billupdated": "", "billupdated": "",
"failedpayment": "", "failedpayment": "",
@@ -116,6 +117,7 @@
"jobassignmentremoved": "", "jobassignmentremoved": "",
"jobchecklist": "", "jobchecklist": "",
"jobconverted": "", "jobconverted": "",
"jobexported": "",
"jobfieldchanged": "", "jobfieldchanged": "",
"jobimported": "", "jobimported": "",
"jobinproductionchange": "", "jobinproductionchange": "",
@@ -128,7 +130,9 @@
"jobspartsorder": "", "jobspartsorder": "",
"jobspartsreturn": "", "jobspartsreturn": "",
"jobstatuschange": "", "jobstatuschange": "",
"jobsupplement": "" "jobsupplement": "",
"jobsuspend": "",
"jobvoid": ""
} }
}, },
"billlines": { "billlines": {
@@ -760,6 +764,7 @@
"driverinformation": "", "driverinformation": "",
"findcontract": "", "findcontract": "",
"findermodal": "", "findermodal": "",
"insuranceexpired": "",
"noteconvertedfrom": "", "noteconvertedfrom": "",
"populatefromjob": "", "populatefromjob": "",
"rates": "", "rates": "",
@@ -887,6 +892,7 @@
"labels": { "labels": {
"bodyhrs": "", "bodyhrs": "",
"dollarsinproduction": "", "dollarsinproduction": "",
"phone": "",
"prodhrs": "", "prodhrs": "",
"refhrs": "" "refhrs": ""
}, },
@@ -902,7 +908,9 @@
"productiondollars": "", "productiondollars": "",
"productionhours": "", "productionhours": "",
"projectedmonthlysales": "", "projectedmonthlysales": "",
"scheduledindate": "",
"scheduledintoday": "", "scheduledintoday": "",
"scheduledoutdate": "",
"scheduledouttoday": "" "scheduledouttoday": ""
} }
}, },
@@ -2674,6 +2682,13 @@
"advanced_filters_hide": "", "advanced_filters_hide": "",
"advanced_filters_filters": "", "advanced_filters_filters": "",
"advanced_filters_sorters": "", "advanced_filters_sorters": "",
"advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "",
"advanced_filters_true": "",
"advanced_filters_false": "",
"advanced_filters_sorter_direction": "",
"advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "",
"dates": "", "dates": "",
"employee": "", "employee": "",
"filterson": "", "filterson": "",
@@ -2849,6 +2864,7 @@
"allemployeetimetickets": "", "allemployeetimetickets": "",
"asoftodaytarget": "", "asoftodaytarget": "",
"body": "", "body": "",
"bodyabbrev": "",
"bodycharttitle": "", "bodycharttitle": "",
"calendarperiod": "", "calendarperiod": "",
"combinedcharttitle": "", "combinedcharttitle": "",
@@ -2865,6 +2881,7 @@
"productivestatistics": "", "productivestatistics": "",
"productivetimeticketsoverdate": "", "productivetimeticketsoverdate": "",
"refinish": "", "refinish": "",
"refinishabbrev": "",
"refinishcharttitle": "", "refinishcharttitle": "",
"targets": "", "targets": "",
"thismonth": "", "thismonth": "",
@@ -2872,6 +2889,7 @@
"timetickets": "", "timetickets": "",
"timeticketsemployee": "", "timeticketsemployee": "",
"todateactual": "", "todateactual": "",
"total": "",
"totalhrs": "", "totalhrs": "",
"totaloverperiod": "", "totaloverperiod": "",
"weeklyactual": "", "weeklyactual": "",

View File

@@ -14,6 +14,8 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }), i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }),
appointmentinsert: (start) => appointmentinsert: (start) =>
i18n.t("audit_trail.messages.appointmentinsert", { start }), i18n.t("audit_trail.messages.appointmentinsert", { start }),
billdeleted: (invoice_number) =>
i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
billposted: (invoice_number) => billposted: (invoice_number) =>
i18n.t("audit_trail.messages.billposted", { invoice_number }), i18n.t("audit_trail.messages.billposted", { invoice_number }),
billupdated: (invoice_number) => billupdated: (invoice_number) =>
@@ -26,6 +28,7 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }), i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
jobconverted: (ro_number) => jobconverted: (ro_number) =>
i18n.t("audit_trail.messages.jobconverted", { ro_number }), i18n.t("audit_trail.messages.jobconverted", { ro_number }),
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
jobfieldchange: (field, value) => jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
jobimported: () => i18n.t("audit_trail.messages.jobimported"), jobimported: () => i18n.t("audit_trail.messages.jobimported"),
@@ -53,6 +56,8 @@ const AuditTrailMapping = {
jobstatuschange: (status) => jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", { status }), i18n.t("audit_trail.messages.jobstatuschange", { status }),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), 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; export default AuditTrailMapping;

View File

@@ -24,4 +24,13 @@ const range = {
], ],
"Last 90 Days": [moment().add(-90, "days"), moment()], "Last 90 Days": [moment().add(-90, "days"), moment()],
}; };
// We are development, lets get crazy
if (process.env.NODE_ENV === "development") {
range["Last year"] = [
moment().subtract(1, "year"),
moment(),
];
}
export default range; export default range;

View File

@@ -9,7 +9,7 @@ import {store} from "../redux/store";
import client from "../utils/GraphQLClient"; import client from "../utils/GraphQLClient";
import cleanAxios from "./CleanAxios"; import cleanAxios from "./CleanAxios";
import {TemplateList} from "./TemplateConstants"; import {TemplateList} from "./TemplateConstants";
import {applyFilters, applySorters, parseQuery, printQuery, wrapFiltersInAnd} from "./graphQLmodifier"; import {generateTemplate} from "./graphQLmodifier";
const server = process.env.REACT_APP_REPORTS_SERVER_URL; const server = process.env.REACT_APP_REPORTS_SERVER_URL;
@@ -75,7 +75,10 @@ export default async function RenderTemplate(
headerpath: `/${bodyshop.imexshopid}/header.html`, headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`, footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop, bodyshop: bodyshop,
filters: templateObject?.filters,
sorters: templateObject?.sorters,
offset: bodyshop.timezone, //dayjs().utcOffset(), offset: bodyshop.timezone, //dayjs().utcOffset(),
defaultSorters: templateObject?.defaultSorters,
}, },
}; };
@@ -278,7 +281,9 @@ export const GenerateDocument = async (
sendType, sendType,
jobid jobid
) => { ) => {
const bodyshop = store.getState().user.bodyshop; const bodyshop = store.getState().user.bodyshop;
if (sendType === "e") { if (sendType === "e") {
store.dispatch( store.dispatch(
setEmailOptions({ setEmailOptions({
@@ -402,9 +407,12 @@ const fetchContextData = async (templateObject, jsrAuth) => {
// console.log('Unmodified Query'); // console.log('Unmodified Query');
// console.dir(templateQueryToExecute); // console.dir(templateQueryToExecute);
const hasFilters = templateObject?.filters?.length > 0;
const hasSorters = templateObject?.sorters?.length > 0;
const hasDefaultSorters = templateObject?.defaultSorters?.length > 0;
// We have no template filters or sorters, so we can just execute the query and return the data // We have no template filters or sorters, so we can just execute the query and return the data
if ((!templateObject?.filters && !templateObject?.filters?.length && !templateObject?.sorters && !templateObject?.sorters?.length)) { if (!hasFilters && !hasSorters && !hasDefaultSorters) {
let contextData = {}; let contextData = {};
if (templateQueryToExecute) { if (templateQueryToExecute) {
const {data} = await client.query({ const {data} = await client.query({
@@ -417,36 +425,11 @@ const fetchContextData = async (templateObject, jsrAuth) => {
return {contextData, useShopSpecificTemplate}; return {contextData, useShopSpecificTemplate};
} }
// Parse the query and apply the filters and sorters return await generateTemplate(
const ast = parseQuery(templateQueryToExecute); templateQueryToExecute,
templateObject,
let filterFields = []; useShopSpecificTemplate
);
if (templateObject?.filters && templateObject?.filters?.length) {
applyFilters(ast, templateObject.filters, filterFields);
wrapFiltersInAnd(ast, filterFields);
}
if (templateObject?.sorters && templateObject?.sorters?.length) {
applySorters(ast, templateObject.sorters);
}
const finalQuery = printQuery(ast);
// commented out for future revision debugging
// console.log('Modified Query');
// console.log(finalQuery);
let contextData = {};
if (templateQueryToExecute) {
const {data} = await client.query({
query: gql(finalQuery),
variables: {...templateObject.variables},
});
contextData = data;
}
return {contextData, useShopSpecificTemplate};
}; };
//export const displayTemplateInWindow = (html) => { //export const displayTemplateInWindow = (html) => {

View File

@@ -1,32 +1,96 @@
import {Kind, parse, print, visit} from "graphql"; 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 = [ const STRING_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_like", label: "contains"}, {value: "_like", label: "contains"},
{value: "_nlike", label: "does not contain"}, {value: "_nlike", label: "does not contain"},
{value: "_ilike", label: "contains case-insensitive"}, {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 = [ const NUMBER_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"}, {value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"}, {value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"}, {value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"} {value: "_lte", label: "less than or equal"},
{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 = { const operators = {
string: STRING_OPERATORS, string: STRING_OPERATORS,
number: NUMBER_OPERATORS number: NUMBER_OPERATORS,
boolean: BOOLEAN_OPERATORS,
bool: BOOLEAN_OPERATORS,
date: DATE_OPERATORS
}; };
return operators[type]; return operators[type];
} }
/* eslint-disable no-loop-func */
/** /**
* Parse a GraphQL query into an AST * Parse a GraphQL query into an AST
* @param query * @param query
@@ -44,6 +108,49 @@ export function parseQuery(query) {
export function printQuery(query) { export function printQuery(query) {
return print(query); return print(query);
} }
/**
* Generate a template based on the query and object
* @param templateQueryToExecute
* @param templateObject
* @param useShopSpecificTemplate
* @returns {Promise<{contextData: {}, useShopSpecificTemplate}>}
*/
export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) {
// Advanced Filtering and Sorting modifications start here
// Parse the query and apply the filters and sorters
const ast = parseQuery(templateQueryToExecute);
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 * Apply sorters to the AST
* @param ast * @param ast
@@ -83,16 +190,16 @@ export function applySorters(ast, sorters) {
if (!orderByArg) { if (!orderByArg) {
orderByArg = { orderByArg = {
kind: Kind.ARGUMENT, kind: Kind.ARGUMENT,
name: { kind: Kind.NAME, value: 'order_by' }, name: {kind: Kind.NAME, value: 'order_by'},
value: { kind: Kind.OBJECT, fields: [] }, value: {kind: Kind.OBJECT, fields: []},
}; };
currentSelection.arguments.push(orderByArg); currentSelection.arguments.push(orderByArg);
} }
const sorterField = { const sorterField = {
kind: Kind.OBJECT_FIELD, kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: targetFieldName }, name: {kind: Kind.NAME, value: targetFieldName},
value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions value: {kind: Kind.ENUM, value: sorter.direction}, // Adjust if your schema uses a different type for sorting directions
}; };
// Add the new sorter condition // 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 * Apply filters to the AST
* @param ast * @param ast
* @param filters * @param filters
* @returns {ASTNode}
*/ */
export function applyFilters(ast, filters) { export function applyFilters(ast, filters) {
return visit(ast, { return visit(ast, {
@@ -116,152 +272,179 @@ export function applyFilters(ast, filters) {
filters.forEach(filter => { filters.forEach(filter => {
const fieldPath = filter.field.split('.'); const fieldPath = filter.field.split('.');
let topLevel = false; let topLevel = false;
let topLevelSub = false;
// Determine if the filter should be applied at the top level // Determine if the filter should be applied at the top level
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { if (fieldPath.length === 2) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevel = true; topLevel = true;
} }
if (topLevel) { if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
// Construct the filter for a top-level application fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
const targetFieldName = fieldPath[fieldPath.length - 1]; topLevelSub = true;
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);
}
} }
// 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 * Get the GraphQL kind for a value
@@ -269,41 +452,17 @@ export function applyFilters(ast, filters) {
* @returns {Kind|Kind.INT} * @returns {Kind|Kind.INT}
*/ */
function getGraphQLKind(value) { 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; return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
} else if (typeof value === 'boolean') { } else if (typeof value === 'boolean') {
return Kind.BOOLEAN; return Kind.BOOLEAN;
} else if (typeof value === 'string') { } else if (typeof value === 'string') {
return Kind.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 */ /* eslint-enable no-loop-func */

748
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@aws-sdk/client-secrets-manager": "^3.454.0", "@aws-sdk/client-secrets-manager": "^3.454.0",
"@aws-sdk/client-ses": "^3.454.0", "@aws-sdk/client-ses": "^3.454.0",
"@aws-sdk/credential-provider-node": "^3.451.0", "@aws-sdk/credential-provider-node": "^3.451.0",
"@azure/storage-blob": "^12.17.0",
"@opensearch-project/opensearch": "^2.4.0", "@opensearch-project/opensearch": "^2.4.0",
"aws4": "^1.12.0", "aws4": "^1.12.0",
"axios": "^1.6.2", "axios": "^1.6.2",
@@ -53,6 +54,7 @@
"xmlbuilder2": "^3.1.1" "xmlbuilder2": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"source-map-explorer": "^2.5.2" "source-map-explorer": "^2.5.2"
}, },
@@ -696,11 +698,496 @@
"tslib": "^2.3.1" "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", "version": "7.23.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
"integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
"optional": true, "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": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
}, },
@@ -719,6 +1206,97 @@
"node": ">=6.9.0" "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": { "node_modules/@colors/colors": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", "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", "resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz",
"integrity": "sha512-bIcKnH5YmtTYr7S6J3J86dn/rFiklwRpOqbTOQ9C0WMmR9FKHVb3bxs2UYfqEmNb93O4nbA97sb6rtz33i9SyA==" "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": { "node_modules/@jsdoc/salty": {
"version": "0.2.6", "version": "0.2.6",
"resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz", "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz",
@@ -1107,6 +1733,14 @@
"yarn": "^1.22.10" "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": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1715,6 +2349,29 @@
"node": ">= 6" "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": { "node_modules/@types/body-parser": {
"version": "1.19.5", "version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -1858,6 +2515,15 @@
"undici-types": "~5.26.4" "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": { "node_modules/@types/qs": {
"version": "6.9.10", "version": "6.9.10",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", "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", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" "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": { "node_modules/@xmldom/xmldom": {
"version": "0.8.10", "version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -3308,6 +3982,14 @@
"node": ">=6" "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": { "node_modules/express": {
"version": "4.18.2", "version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -3855,6 +4537,15 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/google-auth-library": {
"version": "8.9.0", "version": "8.9.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz",
@@ -4447,6 +5138,12 @@
"node": "*" "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": { "node_modules/jose": {
"version": "4.15.4", "version": "4.15.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", "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", "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==" "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": { "node_modules/js-yaml": {
"version": "3.14.1", "version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
@@ -4541,6 +5244,18 @@
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
"optional": true "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": { "node_modules/json-2-csv": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.0.1.tgz",
@@ -5458,6 +6173,14 @@
"node": ">= 0.8.0" "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": { "node_modules/process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "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" "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": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -6955,6 +7687,14 @@
"node": ">=0.6.x" "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": { "node_modules/tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

View File

@@ -21,6 +21,7 @@
"@aws-sdk/client-secrets-manager": "^3.454.0", "@aws-sdk/client-secrets-manager": "^3.454.0",
"@aws-sdk/client-ses": "^3.454.0", "@aws-sdk/client-ses": "^3.454.0",
"@aws-sdk/credential-provider-node": "^3.451.0", "@aws-sdk/credential-provider-node": "^3.451.0",
"@azure/storage-blob": "^12.17.0",
"@opensearch-project/opensearch": "^2.4.0", "@opensearch-project/opensearch": "^2.4.0",
"aws4": "^1.12.0", "aws4": "^1.12.0",
"axios": "^1.6.2", "axios": "^1.6.2",
@@ -62,6 +63,7 @@
"xmlbuilder2": "^3.1.1" "xmlbuilder2": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"source-map-explorer": "^2.5.2" "source-map-explorer": "^2.5.2"
} }